Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/api/src/hackproof/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ class Settings(BaseSettings):
github_tokens: Annotated[list[str], NoDecode] = Field(
default_factory=list, validation_alias="HACKPROOF_GITHUB_TOKEN"
)
opt_out_github_token: str | None = Field(
default=None,
validation_alias=AliasChoices("HACKPROOF_OPT_OUT_GITHUB_TOKEN", "OPT_OUT_GITHUB_TOKEN"),
)
cors_origins: list[str] = ["http://localhost:5173"]

@field_validator("github_tokens", mode="before")
Expand Down
15 changes: 15 additions & 0 deletions apps/api/src/hackproof/export/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,21 @@ async def build_export(
) -> HackathonExport:
window = hackathon.window
projects = await connection.fetch(PROJECTS_SQL, list(tiers), list(TIER_ORDER))

hidden = set(hackathon.hidden)
for h in hackathon.hidden:
if h.startswith("repo:"):
hidden.add(h[5:])
elif not h.startswith("acct:"):
hidden.add(f"repo:{h}")

projects = [
row
for row in projects
if row["full_name"] not in hidden
and f"repo:{row['full_name']}" not in hidden
and (not row["owner"] or row["owner"] not in hidden)
]
names = [row["full_name"] for row in projects]
span = (window.start, window.end)

Expand Down
12 changes: 12 additions & 0 deletions apps/api/src/hackproof/hackathon.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class Hackathon(BaseModel):
window: Window
awards: list[Award] = []
discovery: Discovery
hidden: list[str] = []


@lru_cache
Expand All @@ -46,3 +47,14 @@ def get_hackathon() -> Hackathon:

def places() -> dict[str, Place]:
return {award.project_id: award.place for award in get_hackathon().awards}


def hidden_ids() -> list[str]:
raw = get_hackathon().hidden
ids = set(raw)
for item in raw:
if item.startswith("repo:"):
ids.add(item[5:])
elif not item.startswith("acct:"):
ids.add(f"repo:{item}")
return sorted(ids)
113 changes: 95 additions & 18 deletions apps/api/src/hackproof/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import Any, Literal

from hackproof.db import pool
from hackproof.hackathon import get_hackathon, places
from hackproof.hackathon import get_hackathon, hidden_ids, places
from hackproof.integrations.catalog import get_catalog
from hackproof.linking.methods import METHOD_RANK_SQL

Expand Down Expand Up @@ -31,6 +31,8 @@
SELECT 1 FROM repo_integrations i
WHERE i.full_name = p.full_name AND i.partner = ANY($8)))
AND ($9::text[] IS NULL OR p.project_id = ANY($9))
AND ($10::text[] IS NULL OR NOT (
p.project_id = ANY($10) OR p.full_name = ANY($10) OR r.owner = ANY($10)))
ORDER BY array_position(
ARRAY['verified', 'likely', 'possible', 'unattributed'], p.tier),
p.contracts DESC, p.classic_ops DESC, r.stars DESC NULLS LAST, p.last_activity DESC
Expand All @@ -50,6 +52,12 @@
OR c.wasm_hash ILIKE '%' || $3 || '%')
AND ($4::text IS NULL OR c.deployer = $4)
AND ($5::text IS NULL OR COALESCE('repo:' || a.full_name, 'acct:' || c.deployer) = $5)
AND ($8::text[] IS NULL OR NOT (
COALESCE('repo:' || a.full_name, 'acct:' || c.deployer) = ANY($8)
OR a.full_name = ANY($8)
OR 'acct:' || c.deployer = ANY($8)
OR c.deployer = ANY($8)
))
ORDER BY c.deployed_at DESC
LIMIT $6 OFFSET $7
"""
Expand All @@ -70,6 +78,12 @@
AND ($4::text IS NULL OR w.uploader = $4)
AND ($5::text IS NULL
OR COALESCE('repo:' || COALESCE(aa.full_name, aw.full_name), 'acct:' || w.uploader) = $5)
AND ($8::text[] IS NULL OR NOT (
COALESCE('repo:' || COALESCE(aa.full_name, aw.full_name), 'acct:' || w.uploader) = ANY($8)
OR COALESCE(aa.full_name, aw.full_name) = ANY($8)
OR 'acct:' || w.uploader = ANY($8)
OR w.uploader = ANY($8)
))
ORDER BY w.uploaded_at DESC
LIMIT $6 OFFSET $7
"""
Expand All @@ -86,35 +100,69 @@
OR f.result_code ILIKE '%' || $3 || '%')
AND ($4::text IS NULL OR f.source_account = $4)
AND ($5::text IS NULL OR COALESCE(ap.project_id, 'acct:' || f.source_account) = $5)
AND ($8::text[] IS NULL OR NOT (
COALESCE(ap.project_id, 'acct:' || f.source_account) = ANY($8)
OR 'acct:' || f.source_account = ANY($8)
OR f.source_account = ANY($8)
))
ORDER BY f.created_at DESC
LIMIT $6 OFFSET $7
"""

STATS_SQL = """
SELECT
(SELECT count(*) FROM projects WHERE tier IN ('verified', 'likely')) AS projects,
(SELECT count(*) FROM projects WHERE tier = 'verified') AS verified_projects,
(SELECT count(*) FROM projects WHERE tier = 'likely') AS likely_projects,
(SELECT count(*) FROM projects WHERE tier = 'possible') AS possible_projects,
(SELECT count(*) FROM projects WHERE tier = 'unattributed') AS unattributed_accounts,
(SELECT count(*) FROM contracts WHERE deployed_at BETWEEN $1 AND $2) AS contracts,
(SELECT count(*) FROM projects WHERE tier IN ('verified', 'likely')
AND ($3::text[] IS NULL OR NOT (project_id = ANY($3) OR full_name = ANY($3))))
AS projects,
(SELECT count(*) FROM projects WHERE tier = 'verified'
AND ($3::text[] IS NULL OR NOT (project_id = ANY($3) OR full_name = ANY($3))))
AS verified_projects,
(SELECT count(*) FROM projects WHERE tier = 'likely'
AND ($3::text[] IS NULL OR NOT (project_id = ANY($3) OR full_name = ANY($3))))
AS likely_projects,
(SELECT count(*) FROM projects WHERE tier = 'possible'
AND ($3::text[] IS NULL OR NOT (project_id = ANY($3) OR full_name = ANY($3))))
AS possible_projects,
(SELECT count(*) FROM projects WHERE tier = 'unattributed'
AND ($3::text[] IS NULL OR NOT (project_id = ANY($3) OR full_name = ANY($3))))
AS unattributed_accounts,
(SELECT count(*) FROM contracts c WHERE deployed_at BETWEEN $1 AND $2
AND ($3::text[] IS NULL OR NOT (
'acct:' || c.deployer = ANY($3)
OR c.deployer = ANY($3)
OR c.contract_id IN (
SELECT subject FROM attribution
WHERE subject_kind = 'contract'
AND ('repo:' || full_name = ANY($3) OR full_name = ANY($3))
)
))) AS contracts,
(SELECT count(*) FROM (
SELECT deployer FROM contracts WHERE deployed_at BETWEEN $1 AND $2
UNION SELECT uploader FROM wasm_uploads WHERE uploaded_at BETWEEN $1 AND $2) builders)
AS builders,
UNION SELECT uploader FROM wasm_uploads WHERE uploaded_at BETWEEN $1 AND $2) builders
WHERE $3::text[] IS NULL OR NOT ('acct:' || deployer = ANY($3) OR deployer = ANY($3)))
AS builders,
(SELECT count(DISTINCT c.login) FROM repo_contributors c
JOIN projects p ON p.full_name = c.full_name
WHERE p.tier IN ('verified', 'likely')) AS contributors,
WHERE p.tier IN ('verified', 'likely')
AND ($3::text[] IS NULL OR NOT (p.project_id = ANY($3) OR p.full_name = ANY($3))))
AS contributors,
(SELECT count(DISTINCT country) FROM (
SELECT r.country FROM projects p JOIN repos r ON r.full_name = p.full_name
WHERE p.tier IN ('verified', 'likely')
AND ($3::text[] IS NULL OR NOT (p.project_id = ANY($3) OR p.full_name = ANY($3)))
UNION SELECT u.country FROM projects p
JOIN repo_contributors c ON c.full_name = p.full_name
JOIN github_users u ON u.login = c.login
WHERE p.tier IN ('verified', 'likely')) found
WHERE p.tier IN ('verified', 'likely')
AND ($3::text[] IS NULL OR NOT (p.project_id = ANY($3) OR p.full_name = ANY($3)))) found
WHERE country IS NOT NULL) AS countries,
(SELECT count(*) FROM wasm_uploads WHERE uploaded_at BETWEEN $1 AND $2) AS wasm_uploads,
(SELECT count(*) FROM failed_txs WHERE created_at BETWEEN $1 AND $2) AS failed_txs
(SELECT count(*) FROM wasm_uploads WHERE uploaded_at BETWEEN $1 AND $2
AND ($3::text[] IS NULL OR NOT ('acct:' || uploader = ANY($3) OR uploader = ANY($3))))
AS wasm_uploads,
(SELECT count(*) FROM failed_txs WHERE created_at BETWEEN $1 AND $2
AND ($3::text[] IS NULL OR NOT (
'acct:' || source_account = ANY($3) OR source_account = ANY($3))))
AS failed_txs
"""

LINKS_SQL = f"""
Expand Down Expand Up @@ -147,6 +195,7 @@
SELECT i.partner AS slug, count(DISTINCT i.full_name) AS projects
FROM repo_integrations i
JOIN projects p ON p.full_name = i.full_name AND p.tier IN ('verified', 'likely')
WHERE ($1::text[] IS NULL OR NOT (p.project_id = ANY($1) OR p.full_name = ANY($1)))
GROUP BY i.partner
"""

Expand All @@ -163,6 +212,7 @@
JOIN projects p ON p.full_name = c.full_name AND p.tier IN ('verified', 'likely')
JOIN github_users u ON u.login = c.login
WHERE u.country IS NOT NULL
AND ($2::text[] IS NULL OR NOT (p.project_id = ANY($2) OR p.full_name = ANY($2)))
GROUP BY u.country
ORDER BY contributors DESC, u.country
LIMIT $1
Expand All @@ -183,6 +233,19 @@ def window() -> tuple[datetime, datetime]:
return window.start, window.end


def is_hidden(target: str | None) -> bool:
if not target:
return False
hidden = set(hidden_ids())
return (
target in hidden
or f"repo:{target}" in hidden
or f"acct:{target}" in hidden
or target.removeprefix("repo:") in hidden
or target.removeprefix("acct:") in hidden
)


async def list_page(
resource: Resource,
*,
Expand All @@ -196,12 +259,15 @@ async def list_page(
limit: int = 50,
offset: int = 0,
) -> tuple[list[dict[str, Any]], int]:
if is_hidden(project_id) or is_hidden(account):
return [], 0
hidden = hidden_ids() or None
awards = places()
winner_ids = list(awards) if winners else None
args = (
(q, account, project_id, tiers, limit, offset, track, integrations, winner_ids)
(q, account, project_id, tiers, limit, offset, track, integrations, winner_ids, hidden)
if resource == "projects"
else (*window(), q, account, project_id, limit, offset)
else (*window(), q, account, project_id, limit, offset, hidden)
)
async with pool().acquire() as connection:
rows = await connection.fetch(RESOURCE_SQL[resource], *args)
Expand All @@ -214,16 +280,21 @@ async def list_page(

async def fetch_stats() -> dict[str, int]:
start, end = window()
hidden = hidden_ids() or None
async with pool().acquire() as connection:
return dict(await connection.fetchrow(STATS_SQL, start, end))
return dict(await connection.fetchrow(STATS_SQL, start, end, hidden))


async def fetch_links(full_name: str) -> list[dict[str, Any]]:
if is_hidden(full_name):
return []
async with pool().acquire() as connection:
return [dict(row) for row in await connection.fetch(LINKS_SQL, full_name)]


async def fetch_contributors(full_name: str) -> list[dict[str, Any]]:
if is_hidden(full_name):
return []
async with pool().acquire() as connection:
return [dict(row) for row in await connection.fetch(CONTRIBUTORS_SQL, full_name)]

Expand All @@ -235,15 +306,20 @@ async def fetch_timeline() -> list[dict[str, Any]]:


async def fetch_integration_uses(full_name: str) -> list[dict[str, Any]]:
if is_hidden(full_name):
return []
async with pool().acquire() as connection:
rows = await connection.fetch(INTEGRATION_USES_SQL, full_name)
names = {partner.slug: partner.name for partner in get_catalog().partners}
return [{**row, "name": names[row["slug"]]} for row in map(dict, rows) if row["slug"] in names]


async def fetch_integrations() -> list[dict[str, Any]]:
hidden = hidden_ids() or None
async with pool().acquire() as connection:
counts = {row["slug"]: row["projects"] for row in await connection.fetch(INTEGRATIONS_SQL)}
counts = {
row["slug"]: row["projects"] for row in await connection.fetch(INTEGRATIONS_SQL, hidden)
}
return [
{
**partner.model_dump(include={"slug", "name", "category", "site"}),
Expand All @@ -254,5 +330,6 @@ async def fetch_integrations() -> list[dict[str, Any]]:


async def fetch_countries(limit: int = 12) -> list[dict[str, Any]]:
hidden = hidden_ids() or None
async with pool().acquire() as connection:
return [dict(row) for row in await connection.fetch(COUNTRIES_SQL, limit)]
return [dict(row) for row in await connection.fetch(COUNTRIES_SQL, limit, hidden)]
71 changes: 71 additions & 0 deletions apps/api/src/hackproof/routes.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from typing import Annotated, Any, Literal

import httpx
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel

from hackproof import queries
from hackproof.config import get_settings
from hackproof.queries import is_hidden
from hackproof.schemas import (
Builder,
Contract,
Expand Down Expand Up @@ -95,6 +99,8 @@ async def _everything(resource: queries.Resource, **filters: str) -> list[dict[s

@router.get("/projects/{project_id:path}")
async def project(project_id: str) -> ProjectDetail:
if is_hidden(project_id):
raise HTTPException(status_code=404, detail="project not found")
found = await _everything("projects", project_id=project_id)
if not found:
raise HTTPException(status_code=404, detail="project not found")
Expand All @@ -117,6 +123,8 @@ async def project(project_id: str) -> ProjectDetail:

@router.get("/builders/{address}")
async def builder(address: str) -> Builder:
if is_hidden(address):
raise HTTPException(status_code=404, detail="no activity in the hack window")
result = Builder(
address=address,
projects=await _everything("projects", account=address),
Expand All @@ -127,3 +135,66 @@ async def builder(address: str) -> Builder:
if not (result.projects or result.contracts or result.wasm_uploads or result.failed_txs):
raise HTTPException(status_code=404, detail="no activity in the hack window")
return result


class OptOutRequest(BaseModel):
project_id: str
handle: str
reason: str | None = None


class OptOutResponse(BaseModel):
status: str = "ok"
message: str = "We honor opt outs within 24h."
issue_url: str | None = None


@router.post("/opt-out")
async def opt_out(req: OptOutRequest) -> OptOutResponse:
project_id = req.project_id.strip()
handle = req.handle.strip().lstrip("@")
if not project_id or not handle:
raise HTTPException(status_code=400, detail="project_id and handle are required")

settings = get_settings()
token = settings.opt_out_github_token or (
settings.github_tokens[0] if settings.github_tokens else None
)

issue_title = f"Opt out request: {project_id}"
issue_body = (
f"### Opt Out Request\n\n"
f"- **Project ID**: {project_id}\n"
f"- **GitHub Handle**: @{handle}\n"
f"- **Reason**: {req.reason or 'Not specified'}\n\n"
f"Please remove this project from the index.\n\n"
f"> We honor opt outs within 24h."
)

issue_url = None
if token:
try:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.post(
"https://api.github.com/repos/devodii/hackproof/issues",
headers={
"Authorization": f"token {token}",
"Accept": "application/vnd.github+json",
"User-Agent": "HackProof-OptOut",
},
json={
"title": issue_title,
"body": issue_body,
"labels": ["opt-out"],
},
)
if resp.status_code == 201:
issue_url = resp.json().get("html_url")
except Exception:
pass

return OptOutResponse(
status="ok",
message="We honor opt outs within 24h.",
issue_url=issue_url,
)
Loading