dfdsfsafdsfsdfasdsf
This commit is contained in:
9
LICENSE
Normal file
9
LICENSE
Normal file
@@ -0,0 +1,9 @@
|
||||
Drel's Shitty Open Source License (DSOS License)
|
||||
|
||||
=================================================
|
||||
|
||||
Whatever it is, it is completely free and cannot be sold even in a modified state, but it can be sold if it's used as a base for other project*.
|
||||
Anyone is free to copy, modify, publish and use this project* commercially or non-commercially.
|
||||
Anyone is NOT required to credit the developer and contributers of this project*. (But it's allowed to do it)
|
||||
|
||||
* It can be software or whatever else, it can be any type of project.
|
||||
498
main.py
Normal file
498
main.py
Normal file
@@ -0,0 +1,498 @@
|
||||
from fastapi import FastAPI, Request, Form, HTTPException
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
import uuid
|
||||
import httpx
|
||||
import json
|
||||
import threading
|
||||
import os
|
||||
import time
|
||||
from PIL import Image
|
||||
import io
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import textwrap
|
||||
from urllib.parse import urlparse
|
||||
|
||||
app = FastAPI()
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
STORAGE_FILE = "storage.json"
|
||||
os.makedirs("static/images", exist_ok=True)
|
||||
|
||||
FONT_PATH = "arial.ttf" # Загрузите шрифт и положите в корень
|
||||
LINE_SPACING = 10 # Больше пространства между строками
|
||||
FONT_SIZE = 15 # Увеличить размер шрифта
|
||||
TEXT_COLOR = (0, 0, 0) # Чёрный текст
|
||||
BG_COLOR = (255, 255, 255) # Белый фон
|
||||
|
||||
# Создаём дефолтный текст если шрифт не найден
|
||||
print('тест на файлик ноу нейм')
|
||||
if not os.path.exists("static/text/no_name.gif"):
|
||||
print('нету ураа')
|
||||
img = Image.new("RGB", (100, 20), BG_COLOR)
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.text((5, 5), "", fill=TEXT_COLOR)
|
||||
img.save("static/text/no_name.gif", "GIF")
|
||||
print('наверно всё')
|
||||
|
||||
def load_storage():
|
||||
try:
|
||||
with open(STORAGE_FILE, "r") as f:
|
||||
return json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
return {}
|
||||
|
||||
def save_storage(data):
|
||||
with open(STORAGE_FILE, "w") as f:
|
||||
json.dump(data, f)
|
||||
|
||||
async def process_avatar(image_url: str, user_id: int):
|
||||
try:
|
||||
async with httpx.AsyncClient(follow_redirects=True) as client:
|
||||
response = await client.get(image_url)
|
||||
response.raise_for_status()
|
||||
|
||||
# Извлекаем реальный ID пользователя из URL инстанса
|
||||
parsed_url = urlparse(image_url)
|
||||
filename = f"{user_id}.gif"
|
||||
filepath = f"static/images/{filename}"
|
||||
|
||||
img = Image.open(io.BytesIO(response.content))
|
||||
img = img.resize((128, 128)).convert("RGB")
|
||||
img.save(filepath, "GIF")
|
||||
|
||||
return f"/static/images/{filename}"
|
||||
except Exception as e:
|
||||
print(f"Avatar error: {str(e)}")
|
||||
# Создаём дефолтную аватарку если её нет
|
||||
default_path = "static/images/default.gif"
|
||||
if not os.path.exists(default_path):
|
||||
img = Image.new("RGB", (128, 128), color="gray")
|
||||
img.save(default_path, "GIF")
|
||||
return default_path
|
||||
|
||||
def cleanup_file(file_path):
|
||||
def delete_file():
|
||||
try:
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
print(f"Deleted {file_path}")
|
||||
except Exception as e:
|
||||
print(f"Error deleting {file_path}: {str(e)}")
|
||||
|
||||
# Запускаем удаление через 5 секунд в отдельном потоке
|
||||
timer = threading.Timer(5.0, delete_file)
|
||||
timer.start()
|
||||
|
||||
def render_text(text: str, max_width: int = 480) -> str:
|
||||
if isinstance(text, int): # Добавляем обработку чисел
|
||||
text = str(text)
|
||||
try:
|
||||
text = translit(text, 'ru', reversed=True)
|
||||
except:
|
||||
pass
|
||||
|
||||
if not text.strip():
|
||||
return "/static/text/no_name.gif"
|
||||
|
||||
try:
|
||||
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
|
||||
except IOError:
|
||||
font = ImageFont.load_default()
|
||||
|
||||
# Новый метод расчёта размеров
|
||||
def get_text_size(fnt, txt):
|
||||
bbox = fnt.getbbox(txt)
|
||||
return (bbox[2] - bbox[0], bbox[3] - bbox[1])
|
||||
|
||||
# Разбиваем текст на строки
|
||||
wrapper = textwrap.TextWrapper(width=max_width // (FONT_SIZE))
|
||||
lines = wrapper.wrap(text)
|
||||
|
||||
# Рассчитываем размер изображения
|
||||
line_height = get_text_size(font, "A")[1] + LINE_SPACING # Добавляем spacing к высоте линии
|
||||
img_width = max(get_text_size(font, line)[0] for line in lines) + 10
|
||||
img_height = (line_height * len(lines)) + 10 # Учитываем суммарный spacing
|
||||
|
||||
# Создаём изображение
|
||||
img = Image.new("RGB", (img_width, img_height), BG_COLOR)
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
# Рендерим текст
|
||||
y = 5
|
||||
for line in lines:
|
||||
draw.text((5, y), line, font=font, fill=TEXT_COLOR)
|
||||
y += line_height
|
||||
|
||||
# Сохраняем в static/text
|
||||
os.makedirs("static/text", exist_ok=True)
|
||||
filename = f"{hash(text)}.gif"
|
||||
img_path = f"static/text/{filename}"
|
||||
img.save(img_path, "GIF")
|
||||
|
||||
full_path = os.path.abspath(img_path)
|
||||
cleanup_file(full_path) # Добавляем в очередь на удаление
|
||||
|
||||
return f"/{img_path}"
|
||||
|
||||
def render_separator():
|
||||
separator = "-" * 32 # 40 дефисов
|
||||
return render_text(separator)
|
||||
|
||||
templates.env.globals["render_text"] = render_text
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def login_page(request: Request):
|
||||
return templates.TemplateResponse("index.html", {"request": request})
|
||||
|
||||
@app.post("/auth")
|
||||
async def authenticate(
|
||||
request: Request,
|
||||
instance: str = Form(...),
|
||||
username: str = Form(...),
|
||||
password: str = Form(...),
|
||||
):
|
||||
if not all([instance, username, password]):
|
||||
return RedirectResponse(url="/?error=1", status_code=303)
|
||||
|
||||
instance = instance if instance.startswith(("http://", "https://")) else f"http://{instance}"
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{instance.rstrip('/')}/token",
|
||||
params={
|
||||
"username": username,
|
||||
"password": password,
|
||||
"grant_type": "password",
|
||||
"client_name": "ovk4webtv"
|
||||
}
|
||||
)
|
||||
response.raise_for_status()
|
||||
token_data = response.json()
|
||||
except Exception as e:
|
||||
return RedirectResponse(url="/?error=2", status_code=303)
|
||||
|
||||
storage = load_storage()
|
||||
device_uuid = str(uuid.uuid4())
|
||||
|
||||
storage[device_uuid] = {
|
||||
"token": token_data["access_token"],
|
||||
"instance": instance.rstrip('/'), # Сохраняем нормализованный URL
|
||||
"user_id": token_data["user_id"]
|
||||
}
|
||||
|
||||
save_storage(storage)
|
||||
|
||||
response = RedirectResponse(url="/profile", status_code=303)
|
||||
response.set_cookie(key="webtv_uuid", value=device_uuid)
|
||||
return response
|
||||
|
||||
@app.get("/profile", response_class=HTMLResponse)
|
||||
async def profile_page(request: Request):
|
||||
device_uuid = request.cookies.get("webtv_uuid")
|
||||
storage = load_storage()
|
||||
|
||||
if not device_uuid or device_uuid not in storage:
|
||||
return RedirectResponse(url="/", status_code=303)
|
||||
|
||||
user_data = storage[device_uuid]
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
# Получаем базовую информацию профиля
|
||||
profile_response = await client.get(
|
||||
f"{user_data['instance'].rstrip('/')}/method/Account.getProfileInfo",
|
||||
params={"access_token": user_data["token"]}
|
||||
)
|
||||
profile_response.raise_for_status()
|
||||
profile_data = profile_response.json()["response"]
|
||||
|
||||
# Получаем полную информацию о пользователе
|
||||
users_response = await client.get(
|
||||
f"{user_data['instance'].rstrip('/')}/method/users.get",
|
||||
params={
|
||||
"access_token": user_data["token"],
|
||||
"user_ids": profile_data["id"],
|
||||
"fields": "photo_200"
|
||||
}
|
||||
)
|
||||
users_response.raise_for_status()
|
||||
user_info = users_response.json()["response"][0]
|
||||
except Exception as e:
|
||||
print(f"Profile error: {str(e)}")
|
||||
return RedirectResponse(url="/?error=3", status_code=303)
|
||||
|
||||
# Используем ID из профиля вместо user_id из токена
|
||||
avatar_url = await process_avatar(user_info["photo_200"], user_info["id"])
|
||||
|
||||
return templates.TemplateResponse("profile.html", {
|
||||
"request": request,
|
||||
"first_name_img": render_text(profile_data.get("first_name", "")),
|
||||
"last_name_img": render_text(profile_data.get("last_name", "")),
|
||||
"user_id": user_info["id"],
|
||||
"avatar": avatar_url
|
||||
})
|
||||
|
||||
# Остальные обработчики остаются без изменений
|
||||
|
||||
@app.get("/token", response_class=HTMLResponse)
|
||||
async def show_token(request: Request):
|
||||
device_uuid = request.cookies.get("webtv_uuid")
|
||||
if not device_uuid or device_uuid not in tokens:
|
||||
raise HTTPException(status_code=400, detail="Сессия не найдена")
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"token.html",
|
||||
{
|
||||
"request": request,
|
||||
"token": tokens[device_uuid],
|
||||
"uuid": device_uuid
|
||||
}
|
||||
)
|
||||
|
||||
@app.get("/feed", response_class=HTMLResponse)
|
||||
async def news_feed(
|
||||
request: Request,
|
||||
page: int = 0,
|
||||
global_feed: bool = False
|
||||
):
|
||||
device_uuid = request.cookies.get("webtv_uuid")
|
||||
storage = load_storage()
|
||||
|
||||
# Проверка авторизации
|
||||
if not device_uuid or device_uuid not in storage:
|
||||
return RedirectResponse(url="/", status_code=303)
|
||||
|
||||
user_data = storage[device_uuid]
|
||||
|
||||
try:
|
||||
# Получаем ленту новостей
|
||||
method = "newsfeed.getGlobal" if global_feed else "newsfeed.get"
|
||||
async with httpx.AsyncClient() as client:
|
||||
feed_response = await client.get(
|
||||
f"{user_data['instance']}/method/{method}",
|
||||
params={
|
||||
"access_token": user_data["token"],
|
||||
"count": 5,
|
||||
"offset": page * 5
|
||||
}
|
||||
)
|
||||
feed_response.raise_for_status()
|
||||
feed_data = feed_response.json()["response"]
|
||||
except Exception as e:
|
||||
print(f"Feed error: {str(e)}")
|
||||
return RedirectResponse(url="/?error=4", status_code=303)
|
||||
|
||||
# Собираем ID пользователей
|
||||
user_ids = set()
|
||||
for item in feed_data["items"]:
|
||||
user_ids.add(item["from_id"])
|
||||
user_ids.add(item["owner_id"])
|
||||
|
||||
# Получаем информацию о пользователях
|
||||
users_info = {}
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
users_response = await client.get(
|
||||
f"{user_data['instance']}/method/users.get",
|
||||
params={
|
||||
"access_token": user_data["token"],
|
||||
"user_ids": ",".join(map(str, user_ids)),
|
||||
"fields": "first_name,last_name"
|
||||
}
|
||||
)
|
||||
users_response.raise_for_status()
|
||||
for user in users_response.json()["response"]:
|
||||
users_info[user["id"]] = user
|
||||
except Exception as e:
|
||||
print(f"Users info error: {str(e)}")
|
||||
return RedirectResponse(url="/?error=5", status_code=303)
|
||||
|
||||
# Формируем данные для отображения
|
||||
processed_posts = []
|
||||
for idx, post in enumerate(feed_data["items"]): # Добавлен enumerate
|
||||
from_user = users_info.get(post["from_id"])
|
||||
owner_user = users_info.get(post["owner_id"])
|
||||
|
||||
if not from_user or not owner_user:
|
||||
continue
|
||||
|
||||
post_text = post.get("text", "")
|
||||
|
||||
if post["from_id"] == post["owner_id"]:
|
||||
author_text = f"{from_user['first_name']} {from_user['last_name']} wrote:"
|
||||
else:
|
||||
author_text = (f"{from_user['first_name']} {from_user['last_name']} "
|
||||
f"posted on {owner_user['first_name']} "
|
||||
f"{owner_user['last_name']}'s wall:")
|
||||
|
||||
processed_posts.append({
|
||||
"author": render_text(author_text),
|
||||
"text": render_text(post_text),
|
||||
"has_attachments": len(post.get("attachments", [])) > 0,
|
||||
"likes": post.get("likes", {}).get("count", 0), # Добавляем количество лайков
|
||||
"separator": render_separator() if idx < len(feed_data["items"]) - 1 else None
|
||||
})
|
||||
|
||||
return templates.TemplateResponse("feed.html", {
|
||||
"request": request,
|
||||
"posts": processed_posts,
|
||||
"current_page": page,
|
||||
"has_next": "next_from" in feed_data,
|
||||
"global_feed": global_feed
|
||||
})
|
||||
|
||||
@app.get("/feed_global", response_class=HTMLResponse)
|
||||
async def global_news_feed(
|
||||
request: Request,
|
||||
page: int = 0,
|
||||
global_feed: bool = False
|
||||
):
|
||||
device_uuid = request.cookies.get("webtv_uuid")
|
||||
storage = load_storage()
|
||||
|
||||
# Проверка авторизации
|
||||
if not device_uuid or device_uuid not in storage:
|
||||
return RedirectResponse(url="/", status_code=303)
|
||||
|
||||
user_data = storage[device_uuid]
|
||||
|
||||
try:
|
||||
# Получаем ленту новостей
|
||||
method = "newsfeed.getGlobal"
|
||||
async with httpx.AsyncClient() as client:
|
||||
feed_response = await client.get(
|
||||
f"{user_data['instance']}/method/{method}",
|
||||
params={
|
||||
"access_token": user_data["token"],
|
||||
"count": 5,
|
||||
"offset": page * 5
|
||||
}
|
||||
)
|
||||
feed_response.raise_for_status()
|
||||
feed_data = feed_response.json()["response"]
|
||||
except Exception as e:
|
||||
print(f"Feed error: {str(e)}")
|
||||
return RedirectResponse(url="/?error=4", status_code=303)
|
||||
|
||||
# Собираем ID пользователей
|
||||
user_ids = set()
|
||||
for item in feed_data["items"]:
|
||||
user_ids.add(item["from_id"])
|
||||
user_ids.add(item["owner_id"])
|
||||
|
||||
# Получаем информацию о пользователях
|
||||
users_info = {}
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
users_response = await client.get(
|
||||
f"{user_data['instance']}/method/users.get",
|
||||
params={
|
||||
"access_token": user_data["token"],
|
||||
"user_ids": ",".join(map(str, user_ids)),
|
||||
"fields": "first_name,last_name"
|
||||
}
|
||||
)
|
||||
users_response.raise_for_status()
|
||||
for user in users_response.json()["response"]:
|
||||
users_info[user["id"]] = user
|
||||
except Exception as e:
|
||||
print(f"Users info error: {str(e)}")
|
||||
return RedirectResponse(url="/?error=5", status_code=303)
|
||||
|
||||
# Формируем данные для отображения
|
||||
processed_posts = []
|
||||
for idx, post in enumerate(feed_data["items"]): # Добавлен enumerate
|
||||
from_user = users_info.get(post["from_id"])
|
||||
owner_user = users_info.get(post["owner_id"])
|
||||
|
||||
if not from_user or not owner_user:
|
||||
continue
|
||||
|
||||
post_text = post.get("text", "")
|
||||
|
||||
if post["from_id"] == post["owner_id"]:
|
||||
author_text = f"{from_user['first_name']} {from_user['last_name']} wrote:"
|
||||
else:
|
||||
author_text = (f"{from_user['first_name']} {from_user['last_name']} "
|
||||
f"posted on {owner_user['first_name']} "
|
||||
f"{owner_user['last_name']}'s wall:")
|
||||
|
||||
processed_posts.append({
|
||||
"author": render_text(author_text),
|
||||
"text": render_text(post_text),
|
||||
"has_attachments": len(post.get("attachments", [])) > 0,
|
||||
"likes": post.get("likes", {}).get("count", 0), # Добавляем количество лайков
|
||||
"separator": render_separator() if idx < len(feed_data["items"]) - 1 else None
|
||||
})
|
||||
|
||||
return templates.TemplateResponse("feed_global.html", {
|
||||
"request": request,
|
||||
"posts": processed_posts,
|
||||
"current_page": page,
|
||||
"has_next": "next_from" in feed_data,
|
||||
"global_feed": global_feed
|
||||
})
|
||||
|
||||
@app.get("/post", response_class=HTMLResponse)
|
||||
async def post_page(request: Request):
|
||||
device_uuid = request.cookies.get("webtv_uuid")
|
||||
storage = load_storage()
|
||||
|
||||
if not device_uuid or device_uuid not in storage:
|
||||
return RedirectResponse(url="/", status_code=303)
|
||||
|
||||
return templates.TemplateResponse("post.html", {
|
||||
"request": request,
|
||||
"error": request.query_params.get("error"),
|
||||
"success": "success" in request.query_params
|
||||
})
|
||||
|
||||
@app.post("/post")
|
||||
async def create_post(request: Request):
|
||||
device_uuid = request.cookies.get("webtv_uuid")
|
||||
storage = load_storage()
|
||||
|
||||
if not device_uuid or device_uuid not in storage:
|
||||
return RedirectResponse(url="/", status_code=303)
|
||||
|
||||
user_data = storage[device_uuid]
|
||||
form_data = await request.form()
|
||||
post_text = form_data.get("text", "").strip()
|
||||
|
||||
if not post_text:
|
||||
return RedirectResponse(url="/post?error=empty", status_code=303)
|
||||
|
||||
try:
|
||||
# Get owner_id from profile info
|
||||
async with httpx.AsyncClient() as client:
|
||||
profile_response = await client.get(
|
||||
f"{user_data['instance']}/method/Account.getProfileInfo",
|
||||
params={"access_token": user_data["token"]}
|
||||
)
|
||||
profile_response.raise_for_status()
|
||||
owner_id = profile_response.json()["response"]["id"]
|
||||
|
||||
# Send post
|
||||
post_response = await client.get(
|
||||
f"{user_data['instance']}/method/wall.post",
|
||||
params={
|
||||
"access_token": user_data["token"],
|
||||
"owner_id": owner_id,
|
||||
"message": post_text
|
||||
}
|
||||
)
|
||||
post_response.raise_for_status()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Post error: {str(e)}")
|
||||
return RedirectResponse(url="/post?error=api", status_code=303)
|
||||
|
||||
return RedirectResponse(url="/post?success=1", status_code=303)
|
||||
|
||||
@app.get("/about", response_class=HTMLResponse)
|
||||
async def about_page(request: Request):
|
||||
return templates.TemplateResponse("about.html", {"request": request})
|
||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
fastapi==0.116.1
|
||||
httpx==0.28.1
|
||||
Pillow==11.3.0
|
||||
BIN
static/deepseek.gif
Normal file
BIN
static/deepseek.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
BIN
static/me.gif
Normal file
BIN
static/me.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
BIN
static/text/no_name.gif
Normal file
BIN
static/text/no_name.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 113 B |
20
templates/about.html
Normal file
20
templates/about.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<TITLE>OpenVK4WebTV - About</TITLE>
|
||||
<STYLE>
|
||||
Body { font-family: Arial; background: #EEE; }
|
||||
.container { width: 300px; margin: 50px auto; padding: 20px; background: white; }
|
||||
.field { margin: 10px 0; }
|
||||
INPUT { width: 100%; padding: 2px; }
|
||||
</STYLE>
|
||||
</HEAD>
|
||||
<BODY>
|
||||
<DIV CLASS="container">
|
||||
<P><B>OpenVK4WebTV</B> is a simple ass OpenVK client for WebTVs (and maybe super old web browsers)</P>
|
||||
<BR>
|
||||
<P>Made by me... and DeepSeek (cuz i suck at everything)</P>
|
||||
<IMG SRC="/static/me.gif"><IMG SRC="/static/deepseek.gif">
|
||||
</DIV>
|
||||
</BODY>
|
||||
</HTML>
|
||||
85
templates/feed.html
Normal file
85
templates/feed.html
Normal file
@@ -0,0 +1,85 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<TITLE>News Feed - OpenVK4WebTV</TITLE>
|
||||
<STYLE>
|
||||
Body {
|
||||
font-family: Arial;
|
||||
background: #EEE;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
padding: 15px;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.post {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.post IMG {
|
||||
display: block;
|
||||
border: 1px solid #DDD;
|
||||
}
|
||||
.separator {
|
||||
height: 2px;
|
||||
background: #DDD;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.pagination {
|
||||
text-align: center;
|
||||
margin: 15px 0;
|
||||
}
|
||||
.pagination A {
|
||||
margin: 0 5px;
|
||||
color: #00F;
|
||||
text-decoration: none;
|
||||
}
|
||||
.likes {
|
||||
margin-top: 5px;
|
||||
color: #666;
|
||||
}
|
||||
</STYLE>
|
||||
</HEAD>
|
||||
<BODY>
|
||||
<DIV CLASS="container">
|
||||
<H3>{% if global_feed %}Global {% endif %}News Feed</H3>
|
||||
|
||||
{% for post in posts %}
|
||||
<DIV CLASS="post">
|
||||
<IMG SRC="{{ post.author }}" ALT="Author"><BR>
|
||||
<IMG SRC="{{ post.text }}" ALT="Post text">
|
||||
|
||||
{% if post.has_attachments %}
|
||||
<DIV CLASS="attachments">
|
||||
[Attachments not supported]
|
||||
</DIV>
|
||||
{% endif %}
|
||||
|
||||
<!-- Блок с лайками -->
|
||||
{% if post.likes > 0 %}
|
||||
<DIV CLASS="likes">
|
||||
<IMG SRC="{{ render_text('Likes: ' + post.likes|string) }}" ALT="Likes">
|
||||
</DIV>
|
||||
{% endif %}
|
||||
|
||||
{% if post.separator %}
|
||||
<BR>
|
||||
<IMG SRC="{{ post.separator }}" ALT="Separator">
|
||||
{% endif %}
|
||||
</DIV>
|
||||
{% endfor %}
|
||||
|
||||
<DIV CLASS="pagination">
|
||||
{% if current_page > 0 %}
|
||||
<A HREF="/feed?page={{ current_page - 1 }}&global={{ 1 if global_feed else 0 }}">Previous</A>
|
||||
{% endif %}
|
||||
{% if has_next %}
|
||||
<A HREF="/feed?page={{ current_page + 1 }}&global={{ 1 if global_feed else 0 }}">Next</A>
|
||||
{% endif %}
|
||||
</DIV>
|
||||
|
||||
<P><A HREF="/profile">Profile</A> | <A HREF="/feed_global">Global Feed</A> | <A HREF="/post">New Post</A></P>
|
||||
</BODY>
|
||||
</HTML>
|
||||
85
templates/feed_global.html
Normal file
85
templates/feed_global.html
Normal file
@@ -0,0 +1,85 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<TITLE>Global News Feed - OpenVK4WebTV</TITLE>
|
||||
<STYLE>
|
||||
Body {
|
||||
font-family: Arial;
|
||||
background: #EEE;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
padding: 15px;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.post {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.post IMG {
|
||||
display: block;
|
||||
border: 1px solid #DDD;
|
||||
}
|
||||
.separator {
|
||||
height: 2px;
|
||||
background: #DDD;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.pagination {
|
||||
text-align: center;
|
||||
margin: 15px 0;
|
||||
}
|
||||
.pagination A {
|
||||
margin: 0 5px;
|
||||
color: #00F;
|
||||
text-decoration: none;
|
||||
}
|
||||
.likes {
|
||||
margin-top: 5px;
|
||||
color: #666;
|
||||
}
|
||||
</STYLE>
|
||||
</HEAD>
|
||||
<BODY>
|
||||
<DIV CLASS="container">
|
||||
<H3>{% if global_feed %}Global {% endif %}News Feed</H3>
|
||||
|
||||
{% for post in posts %}
|
||||
<DIV CLASS="post">
|
||||
<IMG SRC="{{ post.author }}" ALT="Author"><BR>
|
||||
<IMG SRC="{{ post.text }}" ALT="Post text">
|
||||
|
||||
{% if post.has_attachments %}
|
||||
<DIV CLASS="attachments">
|
||||
[Attachments not supported]
|
||||
</DIV>
|
||||
{% endif %}
|
||||
|
||||
<!-- Блок с лайками -->
|
||||
{% if post.likes > 0 %}
|
||||
<DIV CLASS="likes">
|
||||
<IMG SRC="{{ render_text('Likes: ' + post.likes|string) }}" ALT="Likes">
|
||||
</DIV>
|
||||
{% endif %}
|
||||
|
||||
{% if post.separator %}
|
||||
<BR>
|
||||
<IMG SRC="{{ post.separator }}" ALT="Separator">
|
||||
{% endif %}
|
||||
</DIV>
|
||||
{% endfor %}
|
||||
|
||||
<DIV CLASS="pagination">
|
||||
{% if current_page > 0 %}
|
||||
<A HREF="/feed?page={{ current_page - 1 }}">Previous</A>
|
||||
{% endif %}
|
||||
{% if has_next %}
|
||||
<A HREF="/feed?page={{ current_page + 1 }}">Next</A>
|
||||
{% endif %}
|
||||
</DIV>
|
||||
|
||||
<P><A HREF="/profile">Profile</A> | <A HREF="/feed">Personal Feed</A> | <A HREF="/post">New Post</A></P>
|
||||
</BODY>
|
||||
</HTML>
|
||||
49
templates/index.html
Normal file
49
templates/index.html
Normal file
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<TITLE>OpenVK4WebTV</TITLE>
|
||||
<STYLE>
|
||||
Body { font-family: Arial; background: #EEE; }
|
||||
.container { width: 300px; margin: 50px auto; padding: 20px; background: white; }
|
||||
.field { margin: 10px 0; }
|
||||
INPUT { width: 100%; padding: 2px; }
|
||||
</STYLE>
|
||||
</HEAD>
|
||||
<BODY>
|
||||
<DIV CLASS="container">
|
||||
<H3>OpenVK4WebTV</H3>
|
||||
{% if request.query_params.get('error') %}
|
||||
<DIV STYLE="color: red; margin: 10px 0;">
|
||||
{% if request.query_params.get('error') == '1' %}
|
||||
All fields are required!
|
||||
{% elif request.query_params.get('error') == '2' %}
|
||||
Authentication failed!
|
||||
{% elif request.query_params.get('error') == '3' %}
|
||||
Profile loading error!
|
||||
{% elif request.query_params.get('error') == '4' %}
|
||||
Feed loading error!
|
||||
{% elif request.query_params.get('error') == '5' %}
|
||||
Users info loading error!
|
||||
{% endif %}
|
||||
</DIV>
|
||||
{% endif %}
|
||||
<FORM METHOD="POST" ACTION="/auth">
|
||||
<DIV CLASS="field">
|
||||
Instance URL:<BR>
|
||||
<INPUT TYPE="text" NAME="instance">
|
||||
</DIV>
|
||||
<DIV CLASS="field">
|
||||
Email:<BR>
|
||||
<INPUT TYPE="text" NAME="username">
|
||||
</DIV>
|
||||
<DIV CLASS="field">
|
||||
Password:<BR>
|
||||
<INPUT TYPE="password" NAME="password">
|
||||
</DIV>
|
||||
<INPUT TYPE="submit" VALUE="Login" STYLE="width: auto; padding: 3px 10px;">
|
||||
<P><A HREF="/profile">Profile (if already logged in)</A></P>
|
||||
<P><A HREF="/about">About</A></P>
|
||||
</FORM>
|
||||
</DIV>
|
||||
</BODY>
|
||||
</HTML>
|
||||
40
templates/post.html
Normal file
40
templates/post.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<TITLE>New Post - OpenVK4WebTV</TITLE>
|
||||
<STYLE>
|
||||
Body { font-family: Arial; background: #EEE; }
|
||||
.container { width: 500px; margin: 20px auto; padding: 15px; background: white; }
|
||||
TEXTAREA { width: 100%; height: 100px; margin: 10px 0; }
|
||||
</STYLE>
|
||||
</HEAD>
|
||||
<BODY>
|
||||
<DIV CLASS="container">
|
||||
<H3>Create New Post</H3>
|
||||
|
||||
{% if error %}
|
||||
<DIV STYLE="color: red; margin: 10px 0;">
|
||||
{% if error == 'empty' %}Post text is required!
|
||||
{% elif error == 'api' %}Posting failed!
|
||||
{% endif %}
|
||||
</DIV>
|
||||
{% endif %}
|
||||
|
||||
{% if success %}
|
||||
<DIV STYLE="color: green; margin: 10px 0;">
|
||||
Post published successfully!
|
||||
</DIV>
|
||||
{% endif %}
|
||||
|
||||
<FORM METHOD="POST" ACTION="/post">
|
||||
<TEXTAREA NAME="text" WRAP="physical"></TEXTAREA><BR>
|
||||
<INPUT TYPE="submit" VALUE="Publish">
|
||||
</FORM>
|
||||
|
||||
<P STYLE="margin-top: 15px;">
|
||||
<A HREF="/profile">Profile</A> |
|
||||
<A HREF="/feed">News Feed</A>
|
||||
</P>
|
||||
</DIV>
|
||||
</BODY>
|
||||
</HTML>
|
||||
39
templates/profile.html
Normal file
39
templates/profile.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<TITLE>Profile - OpenVK4WebTV</TITLE>
|
||||
<STYLE>
|
||||
Body { font-family: Arial; background: #EEE; }
|
||||
.container { width: 500px; margin: 20px auto; padding: 15px; background: white; }
|
||||
.avatar { border: 1px solid #999; margin: 10px 0; }
|
||||
.info { margin: 10px 0; }
|
||||
INPUT[type="submit"] {
|
||||
background: #CC0000;
|
||||
color: white;
|
||||
border: 1px solid #990000;
|
||||
padding: 5px 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</STYLE>
|
||||
</HEAD>
|
||||
<BODY>
|
||||
<DIV CLASS="container">
|
||||
<H3>User Profile</H3>
|
||||
|
||||
<DIV CLASS="avatar">
|
||||
<IMG SRC="{{ avatar }}" WIDTH="128" HEIGHT="128" ALT="Avatar">
|
||||
</DIV>
|
||||
|
||||
<DIV CLASS="info">
|
||||
<B>Name:</B><BR>
|
||||
<IMG SRC="{{ first_name_img }}" ALT="First Name">
|
||||
<IMG SRC="{{ last_name_img }}" ALT="Last Name"><BR>
|
||||
<B>User ID:</B> {{ user_id }}
|
||||
</DIV>
|
||||
|
||||
<P><A HREF="/feed">News Feed</A></P>
|
||||
<P><A HREF="/post">New Post</A></P>
|
||||
<P><A HREF="/">Login into other account</A></P>
|
||||
</DIV>
|
||||
</BODY>
|
||||
</HTML>
|
||||
18
templates/token.html
Normal file
18
templates/token.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Token</title>
|
||||
<style>
|
||||
body { font-family: Arial; padding: 20px; }
|
||||
.token-box { background: #f5f5f5; padding: 15px; margin: 10px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h3>Auth Success!</h3>
|
||||
<div class="token-box">
|
||||
<strong>Device UUID:</strong> {{ uuid }}<br>
|
||||
<strong>Access Token:</strong> {{ token }}
|
||||
</div>
|
||||
<p>This UUID has been saved in cookies</p>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user