Initial commit

This commit is contained in:
2025-08-20 17:28:58 +03:00
commit 19c8391208
9 changed files with 387 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
__pychache__
config.toml
database.db

10
README.md Normal file
View File

@@ -0,0 +1,10 @@
# PyPolls
## API endpoints
Description | Method | Endpoint
----------- | :-------: | ---------------------------------------
Create new poll | POST | /api/v1/polls/create/<choice/checkboxes>
Vote | POST, PUT | /api/v1/polls/<uuid:poll_id>?id=...<br />/api/v1/polls/<uuid:poll_id>?0=true&1=false&2=true
Get results and variants | GET | /api/v1/polls/<uuid:poll_id>
Stop poll | POST, PUT | /api/v1/stop/<uuid:poll_id>[?token=...] [Authorization: Bearer ...]
Delete poll | DELETE | /api/v1/polls/<uuid:poll_id>[?token=...] [Authorization: Bearer ...]

2
config.example.toml Executable file
View File

@@ -0,0 +1,2 @@
[database]
file = "/path/to/database.db"

59
flake.lock generated Normal file
View File

@@ -0,0 +1,59 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1685518550,
"narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1685399834,
"narHash": "sha256-Lt7//5snriXSdJo5hlVcDkpERL1piiih0UXIz1RUcC4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "58c85835512b0db938600b6fe13cc3e3dc4b364e",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

2
requirements.txt Executable file
View File

@@ -0,0 +1,2 @@
Flask==2.0.2
tomli==2.0.1

5
shell.nix Executable file
View File

@@ -0,0 +1,5 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
# nativeBuildInputs is usually what you want -- tools you need to run
nativeBuildInputs = [ pkgs.buildPackages.python311 pkgs.buildPackages.python311Packages.flask pkgs.buildPackages.python311Packages.tomli pkgs.sqlite ];
}

4
src/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
config.toml
database.db
tr(uuid.uuid4())
__pycache__

265
src/app.py Executable file
View File

@@ -0,0 +1,265 @@
from re import A
from flask import (
Flask,
abort,
request,
jsonify,
render_template,
make_response,
Response,
)
import tomli
import uuid
import json
import random
import string
from db import *
protocol = "http"
domain = "localhost:5000"
with open("config.toml", mode="rb") as fp:
cfg = tomli.load(fp)
initdb(db())
app = Flask(__name__)
# APIs
def get_poll_type(n: int):
if n == 1 or n == 2:
return("choice")
elif n == 3 or n == 9:
return("checkboxes")
else:
return("unknown")
def create(title, creator, description, entrs, polltype, alias = ""):
poll_id = str(uuid.uuid4())
admin_token = ''.join(random.choices(string.ascii_letters + string.digits, k=24))
if creator == None:
creator = ""
if description == None:
description = ""
conn = db()
if polltype == "checkboxes":
polltype = "3"
elif polltype == "choice":
polltype = "1"
for i in entrs:
conn.execute(f"INSERT INTO variants VALUES ('{poll_id}', '{i}', 0)")
conn.execute(
f"INSERT INTO polls VALUES ('{poll_id}', '{admin_token}', '{title}', '{creator}', '{description}', {polltype}, '{alias}', 0)"
)
conn.commit()
conn.close()
return (poll_id, admin_token)
@app.route("/api/v1/polls/create/<string:poll_type>", methods=["POST"])
def api_create(poll_type):
if poll_type != "checkboxes" and poll_type != "choice":
return '{"error":"Uknown poll type"}', 400
if request.is_json == False:
req = request.form.to_dict()
else:
req = request.get_json()
if req.get("title") and req.get("values"):
ret = create(req["title"], req.get("author"), req.get("description"), req["values"], poll_type)
return '{"id":"%s","token":"%s"}' % (ret[0], ret[1]), 200
else:
return '{"error":"Title and values are required"}', 400
@app.route("/api/v1/polls/<uuid:poll_id>", methods=["DELETE"])
def deletepoll(poll_id):
c = db()
conn = c.cursor()
variants = conn.execute(
f"SELECT text,number FROM variants WHERE id = '{str(poll_id)}'"
).fetchall()
if variants == []:
return '{"error":"Poll with id %s doesn\'t exist"}' % poll_id, 404
dict = {}
for i in variants:
dict[i[0]] = i[1]
url_admin_token = request.args.get('token')
header_admin_token = request.headers.get('Authorization')
if url_admin_token:
admin_token = url_admin_token
elif header_admin_token:
t = header_admin_token.split("Bearer ")
if t[0] != "":
admin_token = t[0]
else:
admin_token = t[1]
else:
return '{"error":"Unauthorized"}', 401
db_admin_token = conn.execute("SELECT admin_token FROM polls WHERE id = '%s'" % str(poll_id)).fetchone()[0]
if admin_token != db_admin_token:
return '{"error":"Unauthorized"}', 401
poll = conn.execute("SELECT total,type FROM polls WHERE id = '%s'" % poll_id).fetchone()
conn.execute(
"DELETE FROM polls WHERE id = '%s'" % poll_id
)
conn.execute(
"DELETE FROM variants WHERE id = '%s'" % poll_id
)
c.commit()
c.close()
ret = '{"message":"Poll with id %s deleted successfully","results":%s,"total":%s,"type":"%s"}' % (str(poll_id), dict, poll[0], get_poll_type(poll[1]))
return ret.replace("\'", "\""), 200
@app.route("/api/v1/stop/<uuid:poll_id>", methods=["POST", "PUT"])
def stoppoll(poll_id):
c = db()
conn = c.cursor()
variants = conn.execute(
f"SELECT text,number FROM variants WHERE id = '{str(poll_id)}'"
).fetchall()
if variants == []:
return '{"error":"Poll with id %s doesn\'t exist"}' % poll_id, 404
url_admin_token = request.args.get('token')
header_admin_token = request.headers.get('Authorization')
if url_admin_token:
admin_token = url_admin_token
elif header_admin_token:
t = header_admin_token.split("Bearer ")
if t[0] != "":
admin_token = t[0]
else:
admin_token = t[1]
else:
return '{"error":"Unauthorized"}', 401
db_admin_token = conn.execute("SELECT admin_token FROM polls WHERE id = '%s'" % str(poll_id)).fetchone()[0]
if admin_token != db_admin_token:
return '{"error":"Unauthorized"}', 401
type = conn.execute("SELECT type FROM polls WHERE id = '%s'" % poll_id).fetchone()[0]
if type == 1:
new_type = 2
elif type == 3:
new_type = 9
else:
return "", 500
dict = {}
for i in variants:
dict[i[0]] = i[1]
conn.execute(f"""
UPDATE polls
SET type = '{str(new_type)}'
WHERE id = '{str(poll_id)}';
""")
poll = conn.execute("SELECT total,type FROM polls WHERE id = '%s'" % poll_id).fetchone()
c.commit()
c.close()
ret = '{"message":"Voting %s has ended!","results":%s,"total":%s,"type":"%s"}' % (str(poll_id), dict, poll[0], get_poll_type(poll[1]))
return ret.replace("\'", "\""), 200
@app.route("/api/v1/polls/<uuid:poll_id>", methods=["GET"])
def getvariants(poll_id):
c = db()
conn = c.cursor()
variants = conn.execute(
f"SELECT text,number FROM variants WHERE id = '{str(poll_id)}'"
).fetchall()
arr = []
dict = {}
for i in variants:
arr.append(i[0])
dict[i[0]] = i[1]
if arr == []:
return '{"error":"Poll with id %s doesn\'t exist"}' % poll_id, 404
poll = conn.execute("SELECT total,type FROM polls WHERE id = '%s'" % poll_id).fetchone()
c.commit()
c.close()
ret = '{"variants":%s,"results":%s,"total":%s,"type":"%s"}' % (arr, dict, poll[0], get_poll_type(poll[1]))
return ret.replace("\'", "\""), 200
@app.route("/api/v1/polls/<uuid:poll_id>", methods=["POST", "PUT"])
def vote(poll_id):
if request.cookies.get(str(poll_id)):
return '{"error":"Answer has been already sent by you"}', 429
conn = db()
type = conn.execute("SELECT type FROM polls WHERE id = '%s'" % poll_id).fetchone()[0]
if type == 1:
if not request.args.get("id"):
return '{"error":"ID param is unset"}', 400
ids = [request.args.get("id")]
elif type == 3:
ids = []
if request.args == {}:
return '{"error":"IDs params are unset"}', 400
for id, check in request.args.items():
if check == "true":
ids.append(int(id))
elif type == 2 or type == 9:
return '{"error":"The poll with id %s has ended"}' % str(poll_id), 400
else:
return "", 500
available_variants = conn.execute(
f"SELECT text,number FROM variants WHERE id = '{str(poll_id)}'"
).fetchall()
available_variants_array = []
for i in available_variants:
available_variants_array.append(i[0])
if available_variants_array == []:
return '{"error":"Poll with id %s doesn\'t exist"}' % poll_id, 404
for varid in ids:
varid = int(varid)
if varid >= len(available_variants_array):
return '{"error:":"Poll %s doesn\'t have variant with id %s"}' % (str(poll_id), str(varid))
variant = available_variants[varid]
conn.execute(
"UPDATE variants SET number = number + 1 WHERE id = '%s' and text = '%s';"
% (str(poll_id), variant[0])
)
conn.execute(
"UPDATE polls SET total = total + 1 WHERE id = '%s'"
% str(poll_id)
)
poll = conn.execute("SELECT total,type FROM polls WHERE id = '%s'" % poll_id).fetchone()
dict = {}
available_variants = conn.execute(
f"SELECT text,number FROM variants WHERE id = '{str(poll_id)}'"
).fetchall()
for i in available_variants:
dict[i[0]] = i[1]
conn.commit()
conn.close()
ret = '{"message":"You have successfully voted in %s!","variants":%s,"result":%s,"total":%s,"type":"%s"}' % (str(poll_id), ids, dict, poll[0], get_poll_type(poll[1]))
resp = make_response(ret.replace("\'", "\""))
resp.set_cookie(str(poll_id), str(ids))
return resp, 200
if __name__ == "__main__":
app.run()

37
src/db.py Normal file
View File

@@ -0,0 +1,37 @@
import tomli
import sqlite3
with open("config.toml", mode="rb") as fp:
cfg = tomli.load(fp)
def initdb(conn):
c = conn.cursor()
if c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='polls';").fetchall() == []:
print("Creating table 'polls'")
c.execute("""
CREATE TABLE polls (
id TEXT PRIMARY KEY NOT NULL,
admin_token TEXT NOT NULL,
title TEXT NOT NULL,
creator TEXT,
description TEXT,
type INTEGER NOT NULL,
alias TEXT,
total INTEGER);
""")
if c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='variants';").fetchall() == []:
print("Creating table 'variants'")
c.execute("""
CREATE TABLE variants (
id TEXT NOT NULL,
text TEXT NOT NULL,
number INTEGER NOT NULL);
""")
conn.commit()
conn.close()
def db():
conn = sqlite3.connect(cfg["database"]["file"])
return conn