• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

x4dr / Okysa / 21274859717

23 Jan 2026 04:40AM UTC coverage: 81.616% (-0.8%) from 82.41%
21274859717

push

github

x4dr
env vars

26 of 80 new or added lines in 5 files covered. (32.5%)

3 existing lines in 2 files now uncovered.

2717 of 3329 relevant lines covered (81.62%)

0.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

8.96
/scripts/install.py
1
#!/usr/bin/env python3
2
import argparse
1✔
3
import getpass
1✔
4
import json
1✔
5
import os
1✔
6
import re
1✔
7
import secrets
1✔
8
import subprocess
1✔
9
from pathlib import Path
1✔
10

11

12
def get_input(prompt, default=None):
1✔
13
    if default:
×
14
        res = input(f"{prompt} [{default}]: ").strip()
×
15
        return res if res else default
×
16
    return input(f"{prompt}: ").strip()
×
17

18

19
def get_current_user():
1✔
20
    try:
×
21
        return os.getlogin()
×
22
    except OSError:
×
23
        return getpass.getuser()
×
24

25

26
def detect_domains():
1✔
27
    domains = []
×
28
    sites_enabled = Path("/etc/nginx/sites-enabled")
×
29
    if not sites_enabled.exists():
×
30
        return domains
×
31

32
    for site in sites_enabled.iterdir():
×
33
        try:
×
34
            content = site.read_text()
×
35
            matches = re.findall(r"server_name\s+([^;]+);", content)
×
36
            for match in matches:
×
37
                for domain in match.split():
×
38
                    if domain and not domain.startswith("_"):
×
39
                        domains.append(domain)
×
40
        except Exception:
×
41
            continue
×
42
    return sorted(list(set(domains)))
×
43

44

45
def get_repo_nwo():
1✔
46
    try:
×
47
        res = subprocess.run(
×
48
            ["gh", "repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"],
49
            capture_output=True,
50
            text=True,
51
            check=True,
52
        )
53
        return res.stdout.strip()
×
54
    except subprocess.CalledProcessError:
×
55
        return None
×
56

57

58
def register_webhook(domain, secret):
1✔
59
    print("\n--- GitHub Webhook Registration ---")
×
60
    use_gh = get_input("Use 'gh' CLI to register/update webhook?", "y")
×
61
    if use_gh.lower() != "y":
×
62
        return False
×
63

64
    if subprocess.run(["which", "gh"], capture_output=True).returncode != 0:
×
65
        print("Error: 'gh' CLI not found.")
×
66
        return False
×
67

68
    # Check GH auth status
69
    auth_check = subprocess.run(["gh", "auth", "status"], capture_output=True)
×
70
    if auth_check.returncode != 0:
×
71
        print("GitHub CLI is not authenticated.")
×
72
        if get_input("Run 'gh auth login' now?", "y").lower() == "y":
×
73
            try:
×
74
                subprocess.run(["gh", "auth", "login"], check=True)
×
75
            except subprocess.CalledProcessError:
×
76
                return False
×
77
        else:
78
            return False
×
79

80
    nwo = get_repo_nwo()
×
81
    if not nwo:
×
82
        print("Could not determine repository name.")
×
83
        return False
×
84

85
    webhook_url = f"https://{domain}/webhook"
×
86

87
    # Check for existing webhook
88
    try:
×
89
        hooks_json = subprocess.run(
×
90
            ["gh", "api", f"repos/{nwo}/hooks"],
91
            capture_output=True,
92
            text=True,
93
            check=True,
94
        ).stdout
95
        hooks = json.loads(hooks_json)
×
96
        existing_hook = next(
×
97
            (h for h in hooks if h.get("config", {}).get("url") == webhook_url), None
98
        )
99

100
        hook_data = {
×
101
            "active": True,
102
            "events": ["workflow_run"],
103
            "config": {
104
                "url": webhook_url,
105
                "content_type": "json",
106
                "secret": secret,
107
            },
108
        }
109

110
        if existing_hook:
×
111
            print(f"Found existing webhook (ID: {existing_hook['id']}). Updating...")
×
112
            endpoint = f"repos/{nwo}/hooks/{existing_hook['id']}"
×
113
            method = "PATCH"
×
114
        else:
115
            print("Creating new webhook...")
×
116
            hook_data["name"] = "web"
×
117
            endpoint = f"repos/{nwo}/hooks"
×
118
            method = "POST"
×
119

120
        subprocess.run(
×
121
            ["gh", "api", endpoint, "--method", method, "--input", "-"],
122
            input=json.dumps(hook_data),
123
            text=True,
124
            check=True,
125
        )
126
        print(f"Successfully registered/updated webhook: {webhook_url}")
×
127
        return True
×
128
    except Exception as e:
×
129
        print(f"Failed to manage webhook: {e}")
×
130
        return False
×
131

132

133
def uninstall(okysa_root):
1✔
134
    print("\n--- Uninstalling Okysa Deployment ---")
×
135

136
    # Stop and disable services
137
    services = ["okysa.service", "okysa-webhook.service"]
×
138
    for svc in services:
×
139
        print(f"Stopping and disabling {svc}...")
×
140
        subprocess.run(["sudo", "systemctl", "stop", svc], stderr=subprocess.DEVNULL)
×
141
        subprocess.run(["sudo", "systemctl", "disable", svc], stderr=subprocess.DEVNULL)
×
142
        subprocess.run(
×
143
            ["sudo", "rm", f"/etc/systemd/system/{svc}"], stderr=subprocess.DEVNULL
144
        )
145

146
    # Nginx
147
    sites_available = Path("/etc/nginx/sites-available")
×
148
    if sites_available.exists():
×
149
        for site in sites_available.iterdir():
×
150
            try:
×
151
                content = site.read_text()
×
NEW
152
                if "# Managed by Okysa Installer" in content:
×
153
                    print(f"Removing Nginx config: {site.name}")
×
154
                    subprocess.run(
×
155
                        ["sudo", "rm", f"/etc/nginx/sites-enabled/{site.name}"],
156
                        stderr=subprocess.DEVNULL,
157
                    )
158
                    subprocess.run(["sudo", "rm", str(site)], stderr=subprocess.DEVNULL)
×
159
            except:
×
160
                continue
×
161

162
    # Sudoers
163
    print("Removing sudoers entry...")
×
164
    subprocess.run(
×
165
        ["sudo", "rm", "/etc/sudoers.d/okysa-deploy"], stderr=subprocess.DEVNULL
166
    )
167

168
    subprocess.run(["sudo", "systemctl", "daemon-reload"])
×
169
    print("\nUninstallation complete.")
×
170

171

172
def load_env(env_path):
1✔
NEW
173
    env_vars = {}
×
NEW
174
    if env_path.exists():
×
NEW
175
        with open(env_path, "r") as f:
×
NEW
176
            for line in f:
×
NEW
177
                line = line.strip()
×
NEW
178
                if "=" in line and not line.startswith("#"):
×
NEW
179
                    k, v = line.split("=", 1)
×
NEW
180
                    env_vars[k.strip()] = v.strip().strip('"').strip("'")
×
NEW
181
    return env_vars
×
182

183

184
def save_env(env_path, env_vars):
1✔
NEW
185
    with open(env_path, "w") as f:
×
NEW
186
        for k, v in sorted(env_vars.items()):
×
NEW
187
            f.write(f'{k}="{v}"\n')
×
188

189

190
def main():
1✔
191
    parser = argparse.ArgumentParser(description="Okysa Deployment Installer")
×
192
    parser.add_argument(
×
193
        "--uninstall", action="store_true", help="Uninstall the deployment"
194
    )
195
    args = parser.parse_args()
×
196

197
    script_path = Path(__file__).resolve()
×
198
    okysa_root = script_path.parent.parent
×
NEW
199
    env_path = okysa_root / ".env"
×
NEW
200
    home_dir = str(Path.home())
×
201

202
    if args.uninstall:
×
203
        uninstall(okysa_root)
×
204
        return
×
205

206
    gamepack_root = okysa_root.parent / "GamePack"
×
207
    uv_path = subprocess.getoutput("which uv") or "uv"
×
208

209
    print("--- Path Detection ---")
×
210
    print(f"Okysa Root:    {okysa_root}")
×
211
    if gamepack_root.exists():
×
212
        print(f"GamePack Root: {gamepack_root}")
×
213
    else:
214
        print(f"WARNING: GamePack not found at {gamepack_root}")
×
215
        gamepack_root = Path(
×
216
            get_input("Enter absolute path to GamePack", str(gamepack_root))
217
        )
218

219
    # 2. Gather Configuration
220
    print("\n--- Configuration ---")
×
221
    user = get_input("System user to run the bot", get_current_user())
×
222

NEW
223
    env_vars = load_env(env_path)
×
224

NEW
225
    def to_generic(p):
×
NEW
226
        return str(p).replace(home_dir, "~")
×
227

228
    # Required Env Vars
NEW
229
    env_vars["NOSSI"] = get_input(
×
230
        "NOSSI (domain for web services)", env_vars.get("NOSSI", "nossinet.cc")
231
    )
NEW
232
    env_vars["OLLAMA"] = get_input(
×
233
        "OLLAMA (URL for AI API)",
234
        env_vars.get("OLLAMA", "http://localhost:11434"),
235
    )
NEW
236
    env_vars["WIKI"] = get_input(
×
237
        "WIKI (path to wiki files)",
238
        env_vars.get("WIKI", to_generic(okysa_root.parent / "wiki")),
239
    )
NEW
240
    env_vars["STORAGE"] = get_input(
×
241
        "STORAGE (path to storage JSON)",
242
        env_vars.get("STORAGE", to_generic(okysa_root / "Golconda_storage.json")),
243
    )
NEW
244
    env_vars["DATABASE"] = get_input(
×
245
        "DATABASE (path to SQLite DB)",
246
        env_vars.get("DATABASE", to_generic(okysa_root / "Golconda" / "remind.db")),
247
    )
248

NEW
249
    if "DISCORD_TOKEN" not in env_vars:
×
NEW
250
        token_path = Path.home() / "token.discord"
×
NEW
251
        default_token = ""
×
NEW
252
        if token_path.exists():
×
NEW
253
            default_token = token_path.read_text().strip()
×
NEW
254
        env_vars["DISCORD_TOKEN"] = get_input(
×
255
            "DISCORD_TOKEN", env_vars.get("DISCORD_TOKEN", default_token)
256
        )
257

NEW
258
    webhook_secret = env_vars.get("GITHUB_WEBHOOK_SECRET") or secrets.token_urlsafe(32)
×
NEW
259
    env_vars["GITHUB_WEBHOOK_SECRET"] = webhook_secret
×
260

NEW
261
    save_env(env_path, env_vars)
×
NEW
262
    print(f"Configuration saved to {env_path}")
×
263

264
    detected_domains = detect_domains()
×
265
    if detected_domains:
×
NEW
266
        print("\nDetected domains from Nginx:")
×
267
        for i, d in enumerate(detected_domains):
×
268
            print(f"  {i + 1}. {d}")
×
269
        choice = get_input(
×
270
            f"Choose domain (1-{len(detected_domains)}) or enter new", "1"
271
        )
272
        if choice.isdigit() and 1 <= int(choice) <= len(detected_domains):
×
273
            domain = detected_domains[int(choice) - 1]
×
274
        else:
275
            domain = choice
×
276
    else:
NEW
277
        domain = get_input("Domain for Nginx", env_vars["NOSSI"])
×
278

279
    # 3. Prepare scripts
280
    deploy_sh_path = okysa_root / "scripts" / "deploy.sh"
×
281
    webhook_py_path = okysa_root / "scripts" / "webhook_listener.py"
×
282

283
    # Ensure scripts are executable
UNCOV
284
    os.chmod(deploy_sh_path, 0o755)
×
NEW
285
    os.chmod(webhook_py_path, 0o755)
×
286

287
    # 4. Generate Systemd Units
288
    # Use %h for home directory to avoid hardcoding the username in the unit files
NEW
289
    home_dir = str(Path.home())
×
NEW
290
    okysa_root_generic = str(okysa_root).replace(home_dir, "%h")
×
NEW
291
    env_path_generic = str(env_path).replace(home_dir, "%h")
×
NEW
292
    uv_path_generic = str(uv_path).replace(home_dir, "%h")
×
293

UNCOV
294
    bot_service = f"""[Unit]
×
295
Description=Okysa Discord Bot
296
After=network.target
297

298
[Service]
299
Type=simple
300
User={user}
301
WorkingDirectory={okysa_root_generic}
302
EnvironmentFile={env_path_generic}
303
ExecStart={uv_path_generic} run Okysa.py
304
Restart=always
305

306
[Install]
307
WantedBy=multi-user.target
308
"""
309

310
    webhook_service = f"""[Unit]
×
311
Description=Okysa Webhook Listener
312
After=network.target
313

314
[Service]
315
Type=simple
316
User={user}
317
WorkingDirectory={okysa_root_generic}
318
EnvironmentFile={env_path_generic}
319
ExecStart=/usr/bin/python3 {okysa_root_generic}/scripts/webhook_listener.py
320
Restart=always
321

322
[Install]
323
WantedBy=multi-user.target
324
"""
325

326
    # 5. Generate Nginx Config
NEW
327
    nginx_conf = f"""# Managed by Okysa Installer
×
328
server {{
329
    listen 80;
330
    server_name {domain};
331

332
    location /webhook {{
333
        proxy_pass http://localhost:5000;
334
        proxy_set_header Host $host;
335
        proxy_set_header X-Real-IP $remote_addr;
336
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
337
        proxy_set_header X-Forwarded-Proto $scheme;
338
    }}
339
}}
340
"""
341

342
    print("\n--- Proposed Actions ---")
×
343
    print("1. Write /etc/systemd/system/okysa.service")
×
344
    print("2. Write /etc/systemd/system/okysa-webhook.service")
×
345
    print(f"3. Write /etc/nginx/sites-available/{domain}")
×
346
    print("4. Add sudoers entry for systemctl restart")
×
347

348
    if get_input("Perform these actions? (requires sudo)", "n").lower() == "y":
×
349

350
        def sudo_write(content, path):
×
351
            subprocess.run(
×
352
                ["sudo", "tee", str(path)],
353
                input=content.encode(),
354
                stdout=subprocess.DEVNULL,
355
            )
356

357
        sudo_write(bot_service, "/etc/systemd/system/okysa.service")
×
358
        sudo_write(webhook_service, "/etc/systemd/system/okysa-webhook.service")
×
359
        sudo_write(nginx_conf, f"/etc/nginx/sites-available/{domain}")
×
360

361
        sudoers_line = (
×
362
            f"{user} ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart okysa.service\n"
363
        )
364
        sudo_write(sudoers_line, "/etc/sudoers.d/okysa-deploy")
×
365

366
        if (
×
367
            get_input(f"Link /etc/nginx/sites-enabled/{domain}? (y/n)", "y").lower()
368
            == "y"
369
        ):
370
            subprocess.run(
×
371
                [
372
                    "sudo",
373
                    "ln",
374
                    "-sf",
375
                    f"/etc/nginx/sites-available/{domain}",
376
                    f"/etc/nginx/sites-enabled/{domain}",
377
                ]
378
            )
379

380
        subprocess.run(["sudo", "systemctl", "daemon-reload"])
×
381
        if get_input("Enable and start services now? (y/n)", "y").lower() == "y":
×
382
            subprocess.run(
×
383
                ["sudo", "systemctl", "enable", "--now", "okysa-webhook", "okysa"]
384
            )
385

386
        register_webhook(domain, webhook_secret)
×
387
        print(f"\nInstallation complete. Secret: {webhook_secret}")
×
388
    else:
389
        print("\nInstallation aborted.")
×
390

391

392
if __name__ == "__main__":
1✔
393
    main()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc