File size: 10,552 Bytes
382487e
 
 
 
 
 
 
 
35d87fb
 
382487e
35d87fb
382487e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bde84b9
 
 
 
 
382487e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
import json
import os
import re
import shutil
import asyncio
from huggingface_hub import HfApi, hf_hub_download
import config

import tempfile

DATA_FILE_NAME = "database.json"
LOCAL_PATH = os.path.join(tempfile.gettempdir(), DATA_FILE_NAME)

# In-memory database state
db_state = {
    "admins": [],       # List of admin user IDs (ints)
    "users": [],        # List of authorized user IDs (ints)
    "urls": [],         # List of dicts: {"url": str, "added_by": int}
    "user_metadata": {} # Dict: "user_id_str" -> {"username": str, "name": str}
}

# Helper to extract repository ID from a HF URL
def extract_repo_id(url: str) -> str:
    if not url:
        return None
    url = url.strip()
    if "/" in url and not url.startswith("http"):
        return url
    # Match standard dataset url: huggingface.co/datasets/username/dataset-name
    match = re.search(r"huggingface\.co/datasets/([^/]+)/([^/]+)", url, re.IGNORECASE)
    if match:
        return f"{match.group(1)}/{match.group(2)}"
    return None

repo_id = extract_repo_id(config.HF_DATASET_URL)

def get_api():
    if not config.HF_TOKEN:
        return None
    return HfApi(token=config.HF_TOKEN)

def init_db():
    global db_state
    if not repo_id or not config.HF_TOKEN:
        print("HF_DATASET_URL or HF_TOKEN not configured. Working locally.")
        if os.path.exists(LOCAL_PATH):
            try:
                with open(LOCAL_PATH, "r", encoding="utf-8") as f:
                    loaded = json.load(f)
                    # Migrate or update local state
                    for key in db_state:
                        if key in loaded:
                            db_state[key] = loaded[key]
                print("Loaded database from local file.")
            except Exception as e:
                print(f"Error loading local database: {e}")
        return

    # Sync from Hugging Face Dataset
    try:
        api = get_api()
        # Ensure repository exists
        api.create_repo(repo_id=repo_id, repo_type="dataset", exist_ok=True)
        
        # Download file
        print(f"Downloading database from HF Dataset: {repo_id}")
        downloaded_path = hf_hub_download(
            repo_id=repo_id,
            filename=DATA_FILE_NAME,
            repo_type="dataset",
            token=config.HF_TOKEN
        )
        
        # Copy to local path
        shutil.copy(downloaded_path, LOCAL_PATH)
        
        # Load JSON
        with open(LOCAL_PATH, "r", encoding="utf-8") as f:
            loaded = json.load(f)
            for key in db_state:
                if key in loaded:
                    db_state[key] = loaded[key]
                    
        print("Successfully synced database from Hugging Face.")
    except Exception as e:
        err_msg = str(e)
        if "404" in err_msg or "Entry Not Found" in err_msg:
            print("Database file not found in HF dataset. Initializing a new one.")
        else:
            print(f"Error loading database from HF: {err_msg}. Initializing a new one.")
        if os.path.exists(LOCAL_PATH):
            try:
                with open(LOCAL_PATH, "r", encoding="utf-8") as f:
                    loaded = json.load(f)
                    for key in db_state:
                        if key in loaded:
                            db_state[key] = loaded[key]
            except Exception:
                pass
        save_db()

def save_db():
    global db_state
    # Ensure types and clean duplicates
    db_state["admins"] = list(sorted(list(set(int(uid) for uid in db_state.get("admins", [])))))
    db_state["users"] = list(sorted(list(set(int(uid) for uid in db_state.get("users", [])))))
    
    unique_urls = []
    seen = set()
    for item in db_state.get("urls", []):
        url = item.get("url")
        if url not in seen:
            seen.add(url)
            unique_urls.append({
                "url": url,
                "added_by": int(item.get("added_by"))
            })
    db_state["urls"] = unique_urls

    # Save locally
    try:
        with open(LOCAL_PATH, "w", encoding="utf-8") as f:
            json.dump(db_state, f, indent=2, ensure_ascii=False)
        print("Database saved locally.")
    except Exception as e:
        print(f"Error saving database locally: {e}")
        return

    # Upload to HF
    if not repo_id or not config.HF_TOKEN:
        return

    try:
        api = get_api()
        api.upload_file(
            path_or_fileobj=LOCAL_PATH,
            path_in_repo=DATA_FILE_NAME,
            repo_id=repo_id,
            repo_type="dataset",
            token=config.HF_TOKEN
        )
        print("Database uploaded and synced to Hugging Face dataset.")
    except Exception as e:
        print(f"Error uploading database to Hugging Face: {e}")

async def save_db_async():
    await asyncio.to_thread(save_db)

# Normalization of Hugging Face Space URLs
def normalize_hf_url(url: str) -> str:
    url = url.strip()
    
    # Remove any query parameters or trailing slashes
    url = url.split("?")[0].rstrip("/")
    
    # Case 1: Match standard Hugging Face space URLs
    # e.g., https://huggingface.co/spaces/username/spacename
    # or huggingface.co/spaces/username/spacename/tree/main
    # We want to extract username and spacename.
    hf_co_pattern = r"(?:https?://)?(?:www\.)?huggingface\.co/spaces/([^/]+)/([^/]+)"
    match = re.match(hf_co_pattern, url, re.IGNORECASE)
    if match:
        username = match.group(1).lower().replace("_", "-").replace(".", "-")
        spacename = match.group(2).lower().replace("_", "-").replace(".", "-")
        return f"https://{username}-{spacename}.hf.space"
    
    # Case 2: Match direct hf.space URLs
    # e.g., https://username-spacename.hf.space
    # or username-spacename.hf.space/subpath
    hf_space_pattern = r"(?:https?://)?([^/.]+)\.hf\.space"
    match = re.match(hf_space_pattern, url, re.IGNORECASE)
    if match:
        subdomain = match.group(1).lower().replace("_", "-").replace(".", "-")
        return f"https://{subdomain}.hf.space"
        
    # Case 3: Match simple username/spacename form
    # e.g., username/spacename
    simple_pattern = r"^([^/]+)/([^/]+)$"
    match = re.match(simple_pattern, url)
    if match:
        username = match.group(1).lower().replace("_", "-").replace(".", "-")
        spacename = match.group(2).lower().replace("_", "-").replace(".", "-")
        return f"https://{username}-{spacename}.hf.space"
        
    raise ValueError("Invalid Hugging Face Space URL. Make sure it contains 'huggingface.co/spaces/...' or '<subdomain>.hf.space'")

# Authorization helper functions
def is_super_admin(user_id: int) -> bool:
    return config.SUPER_ADMIN_ID is not None and user_id == config.SUPER_ADMIN_ID

def is_admin(user_id: int) -> bool:
    if is_super_admin(user_id):
        return True
    return user_id in db_state.get("admins", [])

def is_authorized(user_id: int) -> bool:
    if is_admin(user_id):
        return True
    return user_id in db_state.get("users", [])

# User metadata updates
def update_user_metadata(user_id: int, username: str = None, first_name: str = None, last_name: str = None):
    if "user_metadata" not in db_state:
        db_state["user_metadata"] = {}
    
    name = first_name or ""
    if last_name:
        name += f" {last_name}"
    name = name.strip()
    
    user_id_str = str(user_id)
    # Check if there is any change to avoid redundant saves
    existing = db_state["user_metadata"].get(user_id_str)
    if not existing or existing.get("username") != username or existing.get("name") != name:
        db_state["user_metadata"][user_id_str] = {
            "username": username,
            "name": name
        }
        save_db()

def get_user_display(user_id: int) -> str:
    user_id_str = str(user_id)
    metadata = db_state.get("user_metadata", {}).get(user_id_str)
    if metadata:
        username = metadata.get("username")
        name = metadata.get("name")
        if username:
            return f"{name} (@{username}) [`{user_id}`]"
        elif name:
            return f"{name} [`{user_id}`]"
    return f"`{user_id}`"

# User administration functions
def add_admin(user_id: int) -> bool:
    if user_id in db_state["admins"]:
        return False
    db_state["admins"].append(user_id)
    save_db()
    return True

def remove_admin(user_id: int) -> bool:
    if user_id not in db_state["admins"]:
        return False
    db_state["admins"].remove(user_id)
    save_db()
    return True

def add_user(user_id: int) -> bool:
    if user_id in db_state["users"]:
        return False
    db_state["users"].append(user_id)
    save_db()
    return True

def remove_user(user_id: int) -> bool:
    if user_id not in db_state["users"]:
        return False
    db_state["users"].remove(user_id)
    save_db()
    return True

def list_admins() -> list:
    return db_state["admins"]

def list_users() -> list:
    return db_state["users"]

# URL management functions
def add_url(url: str, added_by: int) -> tuple[bool, str]:
    normalized = normalize_hf_url(url)
    # Check if already exists
    for item in db_state["urls"]:
        if item["url"] == normalized:
            return False, normalized
    db_state["urls"].append({
        "url": normalized,
        "added_by": added_by
    })
    save_db()
    return True, normalized

def remove_url(url: str, user_id: int) -> tuple[bool, str]:
    try:
        normalized = normalize_hf_url(url)
    except ValueError:
        normalized = url.strip()

    # Find the URL
    target_idx = -1
    for idx, item in enumerate(db_state["urls"]):
        if item["url"] == normalized or item["url"].rstrip("/") == normalized.rstrip("/"):
            target_idx = idx
            break
            
    if target_idx == -1:
        return False, normalized
        
    item = db_state["urls"][target_idx]
    # Check authorization
    # Super admins and admins can remove any URL.
    # Regular users can only remove their own.
    if not is_admin(user_id) and item["added_by"] != user_id:
        raise PermissionError("You can only remove URLs that you added.")
        
    db_state["urls"].pop(target_idx)
    save_db()
    return True, normalized

def get_urls(user_id: int) -> list:
    # If admin or super admin, return all
    if is_admin(user_id):
        return db_state["urls"]
    # Else, return only owned by this user
    return [item for item in db_state["urls"] if item["added_by"] == user_id]

def get_all_urls() -> list:
    return [item["url"] for item in db_state["urls"]]