omglol-api 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
omglol.py
ADDED
|
@@ -0,0 +1,917 @@
|
|
|
1
|
+
"""
|
|
2
|
+
omg.lol API client
|
|
3
|
+
Focused on weblog management and markdown publishing.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
client = OmgLol(api_key="your_key", address="yourname")
|
|
7
|
+
|
|
8
|
+
# Post a markdown file
|
|
9
|
+
client.post_markdown("my-post.md")
|
|
10
|
+
|
|
11
|
+
# Or post content directly
|
|
12
|
+
client.create_post(title="Hello World", content="# Hello\n\nThis is my post.")
|
|
13
|
+
|
|
14
|
+
# List all posts
|
|
15
|
+
for post in client.list_posts():
|
|
16
|
+
print(post.title, post.location)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
import time
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
import requests
|
|
28
|
+
from dotenv import load_dotenv
|
|
29
|
+
|
|
30
|
+
load_dotenv()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class OmgLolError(Exception):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ── Data models ──────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class Post:
|
|
42
|
+
entry_id: str = ""
|
|
43
|
+
title: str = ""
|
|
44
|
+
slug: str = ""
|
|
45
|
+
location: str = ""
|
|
46
|
+
source: str = ""
|
|
47
|
+
body: str = ""
|
|
48
|
+
date: str = ""
|
|
49
|
+
status: str = ""
|
|
50
|
+
raw: dict = field(default_factory=dict, repr=False)
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def from_dict(cls, data: dict) -> "Post":
|
|
54
|
+
return cls(
|
|
55
|
+
entry_id=data.get("entry") or data.get("entry_id", ""),
|
|
56
|
+
title=data.get("title", ""),
|
|
57
|
+
slug=data.get("slug", ""),
|
|
58
|
+
location=data.get("location", ""),
|
|
59
|
+
source=data.get("source", ""),
|
|
60
|
+
body=data.get("body", ""),
|
|
61
|
+
date=data.get("date", ""),
|
|
62
|
+
status=data.get("status", ""),
|
|
63
|
+
raw=data,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class Paste:
|
|
69
|
+
title: str = ""
|
|
70
|
+
content: str = ""
|
|
71
|
+
modified_on: str = ""
|
|
72
|
+
listed: bool = True
|
|
73
|
+
raw: dict = field(default_factory=dict, repr=False)
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_dict(cls, data: dict) -> "Paste":
|
|
77
|
+
return cls(
|
|
78
|
+
title=data.get("title", ""),
|
|
79
|
+
content=data.get("content", ""),
|
|
80
|
+
modified_on=data.get("modified_on", ""),
|
|
81
|
+
listed=bool(data.get("listed", True)),
|
|
82
|
+
raw=data,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class Status:
|
|
88
|
+
id: str = ""
|
|
89
|
+
emoji: str = ""
|
|
90
|
+
content: str = ""
|
|
91
|
+
created: str = ""
|
|
92
|
+
raw: dict = field(default_factory=dict, repr=False)
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def from_dict(cls, data: dict) -> "Status":
|
|
96
|
+
return cls(
|
|
97
|
+
id=data.get("id", ""),
|
|
98
|
+
emoji=data.get("emoji", ""),
|
|
99
|
+
content=data.get("content", ""),
|
|
100
|
+
created=data.get("created", ""),
|
|
101
|
+
raw=data,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class Purl:
|
|
107
|
+
name: str = ""
|
|
108
|
+
url: str = ""
|
|
109
|
+
listed: bool = True
|
|
110
|
+
raw: dict = field(default_factory=dict, repr=False)
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def from_dict(cls, data: dict) -> "Purl":
|
|
114
|
+
return cls(
|
|
115
|
+
name=data.get("name", ""),
|
|
116
|
+
url=data.get("url", ""),
|
|
117
|
+
listed=bool(data.get("listed", True)),
|
|
118
|
+
raw=data,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class DnsRecord:
|
|
124
|
+
id: str = ""
|
|
125
|
+
record_type: str = ""
|
|
126
|
+
name: str = ""
|
|
127
|
+
data: str = ""
|
|
128
|
+
ttl: int = 3600
|
|
129
|
+
raw: dict = field(default_factory=dict, repr=False)
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def from_dict(cls, data: dict) -> "DnsRecord":
|
|
133
|
+
return cls(
|
|
134
|
+
id=data.get("id", ""),
|
|
135
|
+
record_type=data.get("type", ""),
|
|
136
|
+
name=data.get("name", ""),
|
|
137
|
+
data=data.get("data", ""),
|
|
138
|
+
ttl=int(data.get("ttl", 3600)),
|
|
139
|
+
raw=data,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass
|
|
144
|
+
class NowPage:
|
|
145
|
+
content: str = ""
|
|
146
|
+
listed: bool = True
|
|
147
|
+
updated: str = ""
|
|
148
|
+
raw: dict = field(default_factory=dict, repr=False)
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def from_dict(cls, data: dict) -> "NowPage":
|
|
152
|
+
return cls(
|
|
153
|
+
content=data.get("content", ""),
|
|
154
|
+
listed=bool(data.get("listed", True)),
|
|
155
|
+
updated=data.get("updated", ""),
|
|
156
|
+
raw=data,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class OmgLol:
|
|
161
|
+
BASE_URL = "https://api.omg.lol"
|
|
162
|
+
|
|
163
|
+
def __init__(self, api_key: str, address: str):
|
|
164
|
+
self.address = address
|
|
165
|
+
self.session = requests.Session()
|
|
166
|
+
self.session.headers.update({
|
|
167
|
+
"Authorization": f"Bearer {api_key}",
|
|
168
|
+
"Content-Type": "application/json",
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
MAX_RETRIES = 3
|
|
172
|
+
RETRY_BACKOFF = 1.0 # seconds, doubled each retry
|
|
173
|
+
|
|
174
|
+
def _request(self, method: str, path: str, **kwargs):
|
|
175
|
+
url = f"{self.BASE_URL}{path}"
|
|
176
|
+
last_exc = None
|
|
177
|
+
for attempt in range(self.MAX_RETRIES):
|
|
178
|
+
try:
|
|
179
|
+
resp = self.session.request(method, url, **kwargs)
|
|
180
|
+
if resp.status_code == 429:
|
|
181
|
+
retry_after = float(resp.headers.get("Retry-After", self.RETRY_BACKOFF * 2**attempt))
|
|
182
|
+
time.sleep(retry_after)
|
|
183
|
+
continue
|
|
184
|
+
resp.raise_for_status()
|
|
185
|
+
data = resp.json()
|
|
186
|
+
if not data.get("request", {}).get("success", True):
|
|
187
|
+
raise OmgLolError(data.get("response", {}).get("message", "Unknown error"))
|
|
188
|
+
return data.get("response", data)
|
|
189
|
+
except requests.ConnectionError as exc:
|
|
190
|
+
last_exc = exc
|
|
191
|
+
time.sleep(self.RETRY_BACKOFF * 2**attempt)
|
|
192
|
+
raise last_exc or OmgLolError("Max retries exceeded")
|
|
193
|
+
|
|
194
|
+
# ── Weblog ────────────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
def list_posts(self) -> list[Post]:
|
|
197
|
+
"""Return all weblog entries."""
|
|
198
|
+
response = self._request("GET", f"/address/{self.address}/weblog/entries")
|
|
199
|
+
return [Post.from_dict(e) for e in response.get("entries", [])]
|
|
200
|
+
|
|
201
|
+
def get_post(self, entry_id: str) -> Post:
|
|
202
|
+
"""Fetch a single weblog entry by ID."""
|
|
203
|
+
response = self._request("GET", f"/address/{self.address}/weblog/entry/{entry_id}")
|
|
204
|
+
return Post.from_dict(response.get("entry", response))
|
|
205
|
+
|
|
206
|
+
def get_latest_post(self) -> Post:
|
|
207
|
+
"""Fetch the latest published post (no auth required)."""
|
|
208
|
+
response = self._request("GET", f"/address/{self.address}/weblog/post/latest")
|
|
209
|
+
return Post.from_dict(response.get("post", response))
|
|
210
|
+
|
|
211
|
+
def create_post(
|
|
212
|
+
self,
|
|
213
|
+
title: str,
|
|
214
|
+
content: str,
|
|
215
|
+
date: datetime | None = None,
|
|
216
|
+
slug: str | None = None,
|
|
217
|
+
entry_id: str | None = None,
|
|
218
|
+
) -> Post:
|
|
219
|
+
"""
|
|
220
|
+
Create or update a weblog entry.
|
|
221
|
+
|
|
222
|
+
The omg.lol weblog format uses a frontmatter block delimited by ---:
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
Date: 2025-03-14 12:00
|
|
226
|
+
Slug: optional-custom-slug
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
# Your title here
|
|
230
|
+
|
|
231
|
+
Body content in markdown...
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
title: Post title (used as the H1 heading).
|
|
235
|
+
content: Markdown body (without the title line).
|
|
236
|
+
date: Publication datetime (defaults to now).
|
|
237
|
+
slug: Optional URL slug (defaults to slugified title).
|
|
238
|
+
entry_id: Existing entry ID to update, or None to create new.
|
|
239
|
+
"""
|
|
240
|
+
if date is None:
|
|
241
|
+
date = datetime.now()
|
|
242
|
+
|
|
243
|
+
date_str = date.strftime("%Y-%m-%d %H:%M")
|
|
244
|
+
|
|
245
|
+
if slug is None:
|
|
246
|
+
slug = re.sub(r"[^\w]+", "-", title.lower()).strip("-")
|
|
247
|
+
|
|
248
|
+
# Compose the omg.lol weblog source format with --- delimiters
|
|
249
|
+
frontmatter = f"---\nDate: {date_str}\nSlug: {slug}\n---"
|
|
250
|
+
body = f"# {title}\n\n{content.strip()}"
|
|
251
|
+
source = f"{frontmatter}\n\n{body}"
|
|
252
|
+
|
|
253
|
+
if entry_id is None:
|
|
254
|
+
entry_id = slug # omg.lol accepts a slug as the entry ID for new posts
|
|
255
|
+
|
|
256
|
+
response = self._request(
|
|
257
|
+
"POST",
|
|
258
|
+
f"/address/{self.address}/weblog/entry/{entry_id}",
|
|
259
|
+
data=source,
|
|
260
|
+
headers={"Content-Type": "text/plain"},
|
|
261
|
+
)
|
|
262
|
+
return Post.from_dict(response.get("entry", response))
|
|
263
|
+
|
|
264
|
+
def post_markdown(self, filepath: str | Path, entry_id: str | None = None) -> Post:
|
|
265
|
+
"""
|
|
266
|
+
Publish a markdown file to your omg.lol weblog.
|
|
267
|
+
|
|
268
|
+
The file may optionally start with a YAML-like frontmatter block:
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
title: My Post Title
|
|
272
|
+
date: 2025-03-14 12:00
|
|
273
|
+
slug: my-post-title
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
# My Post Title
|
|
277
|
+
|
|
278
|
+
Body content here...
|
|
279
|
+
|
|
280
|
+
If no frontmatter is present, the first H1 heading is used as
|
|
281
|
+
the title, and the date defaults to now.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
filepath: Path to the .md file.
|
|
285
|
+
entry_id: Existing entry ID to update (optional).
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
The created/updated Post.
|
|
289
|
+
"""
|
|
290
|
+
path = Path(filepath)
|
|
291
|
+
if not path.exists():
|
|
292
|
+
raise FileNotFoundError(f"File not found: {filepath}")
|
|
293
|
+
|
|
294
|
+
raw = path.read_text(encoding="utf-8").strip()
|
|
295
|
+
|
|
296
|
+
# Parse optional frontmatter
|
|
297
|
+
title = None
|
|
298
|
+
date = None
|
|
299
|
+
slug = None
|
|
300
|
+
|
|
301
|
+
if raw.startswith("---"):
|
|
302
|
+
parts = raw.split("---", 2)
|
|
303
|
+
if len(parts) >= 3:
|
|
304
|
+
fm_block = parts[1].strip()
|
|
305
|
+
body = parts[2].strip()
|
|
306
|
+
for line in fm_block.splitlines():
|
|
307
|
+
if ":" in line:
|
|
308
|
+
key, _, val = line.partition(":")
|
|
309
|
+
key = key.strip().lower()
|
|
310
|
+
val = val.strip()
|
|
311
|
+
if key == "title":
|
|
312
|
+
title = val
|
|
313
|
+
elif key == "date":
|
|
314
|
+
try:
|
|
315
|
+
date = datetime.fromisoformat(val)
|
|
316
|
+
except ValueError:
|
|
317
|
+
pass
|
|
318
|
+
elif key == "slug":
|
|
319
|
+
slug = val
|
|
320
|
+
else:
|
|
321
|
+
body = raw
|
|
322
|
+
else:
|
|
323
|
+
body = raw
|
|
324
|
+
|
|
325
|
+
# Fall back to first H1 in the body as title
|
|
326
|
+
if title is None:
|
|
327
|
+
for line in body.splitlines():
|
|
328
|
+
if line.startswith("# "):
|
|
329
|
+
title = line[2:].strip()
|
|
330
|
+
break
|
|
331
|
+
|
|
332
|
+
if title is None:
|
|
333
|
+
title = path.stem.replace("-", " ").replace("_", " ").title()
|
|
334
|
+
|
|
335
|
+
# Strip leading H1 from body to avoid duplication (create_post adds it)
|
|
336
|
+
body_lines = body.splitlines()
|
|
337
|
+
if body_lines and body_lines[0].strip() == f"# {title}":
|
|
338
|
+
body = "\n".join(body_lines[1:]).strip()
|
|
339
|
+
|
|
340
|
+
print(f"Publishing: '{title}' from {path.name}")
|
|
341
|
+
result = self.create_post(title=title, content=body, date=date, slug=slug, entry_id=entry_id)
|
|
342
|
+
print(f" Published at: https://{self.address}.weblog.lol{result.location}")
|
|
343
|
+
return result
|
|
344
|
+
|
|
345
|
+
def delete_post(self, entry_id: str) -> str:
|
|
346
|
+
"""Permanently delete a weblog entry. Returns the confirmation message."""
|
|
347
|
+
response = self._request("DELETE", f"/address/{self.address}/weblog/delete/{entry_id}")
|
|
348
|
+
return response.get("message", "Deleted.")
|
|
349
|
+
|
|
350
|
+
# ── Weblog config ─────────────────────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
def get_weblog_config(self) -> dict:
|
|
353
|
+
"""Retrieve weblog configuration."""
|
|
354
|
+
return self._request("GET", f"/address/{self.address}/weblog/configuration")
|
|
355
|
+
|
|
356
|
+
# ── Pastebin ───────────────────────────────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
def list_pastes(self) -> list[Paste]:
|
|
359
|
+
"""Return all pastes (including unlisted, requires auth)."""
|
|
360
|
+
response = self._request("GET", f"/address/{self.address}/pastebin")
|
|
361
|
+
return [Paste.from_dict(p) for p in response.get("pastes", [])]
|
|
362
|
+
|
|
363
|
+
def get_paste(self, title: str) -> Paste:
|
|
364
|
+
"""Fetch a single paste by title (public, no auth required)."""
|
|
365
|
+
response = self._request("GET", f"/address/{self.address}/pastebin/{title}")
|
|
366
|
+
return Paste.from_dict(response.get("paste", response))
|
|
367
|
+
|
|
368
|
+
def create_paste(self, title: str, content: str) -> Paste:
|
|
369
|
+
"""Create or update a paste."""
|
|
370
|
+
response = self._request(
|
|
371
|
+
"POST",
|
|
372
|
+
f"/address/{self.address}/pastebin/",
|
|
373
|
+
json={"title": title, "content": content},
|
|
374
|
+
)
|
|
375
|
+
return Paste.from_dict(response)
|
|
376
|
+
|
|
377
|
+
def delete_paste(self, title: str) -> str:
|
|
378
|
+
"""Permanently delete a paste. Returns the confirmation message."""
|
|
379
|
+
response = self._request("DELETE", f"/address/{self.address}/pastebin/{title}")
|
|
380
|
+
return response.get("message", "Deleted.")
|
|
381
|
+
|
|
382
|
+
# ── Statuslog ─────────────────────────────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
def post_status(self, content: str, emoji: str = "✍️") -> Status:
|
|
385
|
+
"""Share a new status to your statuslog."""
|
|
386
|
+
response = self._request(
|
|
387
|
+
"POST",
|
|
388
|
+
f"/address/{self.address}/statuses/",
|
|
389
|
+
json={"emoji": emoji, "content": content},
|
|
390
|
+
)
|
|
391
|
+
return Status.from_dict(response)
|
|
392
|
+
|
|
393
|
+
def list_statuses(self) -> list[Status]:
|
|
394
|
+
"""Retrieve all statuses for the address."""
|
|
395
|
+
response = self._request("GET", f"/address/{self.address}/statuses/")
|
|
396
|
+
return [Status.from_dict(s) for s in response.get("statuses", [])]
|
|
397
|
+
|
|
398
|
+
# ── PURLs (Persistent URLs) ────────────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
def list_purls(self) -> list[Purl]:
|
|
401
|
+
"""Return all PURLs for the address."""
|
|
402
|
+
response = self._request("GET", f"/address/{self.address}/purls")
|
|
403
|
+
return [Purl.from_dict(p) for p in response.get("purls", [])]
|
|
404
|
+
|
|
405
|
+
def get_purl(self, name: str) -> Purl:
|
|
406
|
+
"""Fetch a single PURL by name."""
|
|
407
|
+
response = self._request("GET", f"/address/{self.address}/purl/{name}")
|
|
408
|
+
return Purl.from_dict(response.get("purl", response))
|
|
409
|
+
|
|
410
|
+
def create_purl(self, name: str, url: str, listed: bool = True) -> Purl:
|
|
411
|
+
"""Create a new PURL (persistent redirect)."""
|
|
412
|
+
response = self._request(
|
|
413
|
+
"POST",
|
|
414
|
+
f"/address/{self.address}/purl",
|
|
415
|
+
json={"name": name, "url": url, "listed": listed},
|
|
416
|
+
)
|
|
417
|
+
return Purl.from_dict(response)
|
|
418
|
+
|
|
419
|
+
def delete_purl(self, name: str) -> str:
|
|
420
|
+
"""Delete a PURL."""
|
|
421
|
+
response = self._request("DELETE", f"/address/{self.address}/purl/{name}")
|
|
422
|
+
return response.get("message", "Deleted.")
|
|
423
|
+
|
|
424
|
+
# ── Now page ───────────────────────────────────────────────────────────────
|
|
425
|
+
|
|
426
|
+
def get_now(self) -> NowPage:
|
|
427
|
+
"""Retrieve the /now page content (public, no auth required)."""
|
|
428
|
+
response = self._request("GET", f"/address/{self.address}/now")
|
|
429
|
+
return NowPage.from_dict(response.get("now", response))
|
|
430
|
+
|
|
431
|
+
def update_now(self, content: str, listed: bool = True) -> NowPage:
|
|
432
|
+
"""Update the /now page."""
|
|
433
|
+
response = self._request(
|
|
434
|
+
"POST",
|
|
435
|
+
f"/address/{self.address}/now",
|
|
436
|
+
json={"content": content, "listed": "1" if listed else "0"},
|
|
437
|
+
)
|
|
438
|
+
return NowPage.from_dict(response)
|
|
439
|
+
|
|
440
|
+
@staticmethod
|
|
441
|
+
def get_now_garden() -> list[NowPage]:
|
|
442
|
+
"""Retrieve the Now Garden — all listed /now pages (no auth)."""
|
|
443
|
+
resp = requests.get("https://api.omg.lol/now/garden")
|
|
444
|
+
data = resp.json()
|
|
445
|
+
return [NowPage.from_dict(n) for n in data.get("response", {}).get("garden", [])]
|
|
446
|
+
|
|
447
|
+
# ── DNS ────────────────────────────────────────────────────────────────────
|
|
448
|
+
|
|
449
|
+
def list_dns_records(self) -> list[DnsRecord]:
|
|
450
|
+
"""Return all DNS records for the address."""
|
|
451
|
+
response = self._request("GET", f"/address/{self.address}/dns")
|
|
452
|
+
return [DnsRecord.from_dict(r) for r in response.get("dns", [])]
|
|
453
|
+
|
|
454
|
+
def create_dns_record(self, record_type: str, name: str, data: str, ttl: int = 3600) -> DnsRecord:
|
|
455
|
+
"""Create a DNS record."""
|
|
456
|
+
response = self._request(
|
|
457
|
+
"POST",
|
|
458
|
+
f"/address/{self.address}/dns",
|
|
459
|
+
json={"type": record_type, "name": name, "data": data, "ttl": ttl},
|
|
460
|
+
)
|
|
461
|
+
return DnsRecord.from_dict(response)
|
|
462
|
+
|
|
463
|
+
def update_dns_record(self, record_id: str, record_type: str, name: str, data: str, ttl: int = 3600) -> DnsRecord:
|
|
464
|
+
"""Update an existing DNS record."""
|
|
465
|
+
response = self._request(
|
|
466
|
+
"PATCH",
|
|
467
|
+
f"/address/{self.address}/dns/{record_id}",
|
|
468
|
+
json={"type": record_type, "name": name, "data": data, "ttl": ttl},
|
|
469
|
+
)
|
|
470
|
+
return DnsRecord.from_dict(response)
|
|
471
|
+
|
|
472
|
+
def delete_dns_record(self, record_id: str) -> str:
|
|
473
|
+
"""Delete a DNS record."""
|
|
474
|
+
response = self._request("DELETE", f"/address/{self.address}/dns/{record_id}")
|
|
475
|
+
return response.get("message", "Deleted.")
|
|
476
|
+
|
|
477
|
+
# ── Profile picture ─────────────────────────────────────────────────────────
|
|
478
|
+
|
|
479
|
+
def upload_pfp(self, filepath: str | Path) -> str:
|
|
480
|
+
"""Upload a profile picture. Returns the API confirmation message."""
|
|
481
|
+
path = Path(filepath)
|
|
482
|
+
if not path.exists():
|
|
483
|
+
raise FileNotFoundError(f"File not found: {filepath}")
|
|
484
|
+
image_data = path.read_bytes()
|
|
485
|
+
response = self._request(
|
|
486
|
+
"POST",
|
|
487
|
+
f"/address/{self.address}/pfp",
|
|
488
|
+
data=image_data,
|
|
489
|
+
headers={"Content-Type": "application/octet-stream"},
|
|
490
|
+
)
|
|
491
|
+
return response.get("message", "Uploaded.")
|
|
492
|
+
|
|
493
|
+
# ── Web / Profile ──────────────────────────────────────────────────────────
|
|
494
|
+
|
|
495
|
+
def get_web(self) -> dict:
|
|
496
|
+
"""Retrieve the web page / profile content."""
|
|
497
|
+
return self._request("GET", f"/address/{self.address}/web")
|
|
498
|
+
|
|
499
|
+
def update_web(self, content: str, publish: bool = True) -> dict:
|
|
500
|
+
"""Update and optionally publish the web page / profile."""
|
|
501
|
+
payload = {"content": content}
|
|
502
|
+
if publish:
|
|
503
|
+
payload["publish"] = True
|
|
504
|
+
return self._request(
|
|
505
|
+
"POST",
|
|
506
|
+
f"/address/{self.address}/web",
|
|
507
|
+
json=payload,
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
# ── Email ──────────────────────────────────────────────────────────────────
|
|
511
|
+
|
|
512
|
+
def get_email_forwarding(self) -> dict:
|
|
513
|
+
"""Retrieve current email forwarding settings."""
|
|
514
|
+
return self._request("GET", f"/address/{self.address}/email/")
|
|
515
|
+
|
|
516
|
+
def set_email_forwarding(self, destination: str) -> dict:
|
|
517
|
+
"""Set email forwarding destination."""
|
|
518
|
+
return self._request(
|
|
519
|
+
"POST",
|
|
520
|
+
f"/address/{self.address}/email/",
|
|
521
|
+
json={"destination": destination},
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
# ── Account / address info ────────────────────────────────────────────────
|
|
525
|
+
|
|
526
|
+
def get_address_info(self) -> dict:
|
|
527
|
+
"""Get private info about the address."""
|
|
528
|
+
return self._request("GET", f"/address/{self.address}/info")
|
|
529
|
+
|
|
530
|
+
def get_service_info(self) -> dict:
|
|
531
|
+
"""Get public omg.lol service stats (no auth needed)."""
|
|
532
|
+
return self._request("GET", "/service/info")
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
# ── CLI ──────────────────────────────────────────────────────────────────────
|
|
536
|
+
|
|
537
|
+
import argparse
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _get_client() -> OmgLol:
|
|
541
|
+
api_key = os.environ.get("OMGLOL_API_KEY")
|
|
542
|
+
address = os.environ.get("OMGLOL_ADDRESS")
|
|
543
|
+
if not api_key or not address:
|
|
544
|
+
print("Error: Set OMGLOL_API_KEY and OMGLOL_ADDRESS in .env or environment")
|
|
545
|
+
sys.exit(1)
|
|
546
|
+
return OmgLol(api_key=api_key, address=address)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
# ── Weblog commands ──────────────────────────────────────────────────────────
|
|
550
|
+
|
|
551
|
+
def cmd_post(args: argparse.Namespace) -> None:
|
|
552
|
+
client = _get_client()
|
|
553
|
+
for f in args.files:
|
|
554
|
+
if args.dry_run:
|
|
555
|
+
print(f"[dry-run] Would publish: {f}")
|
|
556
|
+
continue
|
|
557
|
+
try:
|
|
558
|
+
client.post_markdown(f)
|
|
559
|
+
except Exception as e:
|
|
560
|
+
print(f" Error publishing {f}: {e}", file=sys.stderr)
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def cmd_posts_list(args: argparse.Namespace) -> None:
|
|
564
|
+
client = _get_client()
|
|
565
|
+
for post in client.list_posts():
|
|
566
|
+
print(f" {post.date:16s} {post.title} ({post.slug})")
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def cmd_posts_get(args: argparse.Namespace) -> None:
|
|
570
|
+
client = _get_client()
|
|
571
|
+
post = client.get_post(args.entry_id)
|
|
572
|
+
print(f"Title: {post.title}")
|
|
573
|
+
print(f"Date: {post.date}")
|
|
574
|
+
print(f"Slug: {post.slug}")
|
|
575
|
+
print(f"URL: {post.location}")
|
|
576
|
+
print()
|
|
577
|
+
print(post.body or post.source)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def cmd_posts_delete(args: argparse.Namespace) -> None:
|
|
581
|
+
if args.dry_run:
|
|
582
|
+
print(f"[dry-run] Would delete post: {args.entry_id}")
|
|
583
|
+
return
|
|
584
|
+
client = _get_client()
|
|
585
|
+
print(client.delete_post(args.entry_id))
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
# ── Paste commands ───────────────────────────────────────────────────────────
|
|
589
|
+
|
|
590
|
+
def cmd_paste_list(args: argparse.Namespace) -> None:
|
|
591
|
+
client = _get_client()
|
|
592
|
+
for paste in client.list_pastes():
|
|
593
|
+
print(f" {paste.title:30s} {paste.modified_on}")
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def cmd_paste_get(args: argparse.Namespace) -> None:
|
|
597
|
+
client = _get_client()
|
|
598
|
+
paste = client.get_paste(args.title)
|
|
599
|
+
print(paste.content)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def cmd_paste_create(args: argparse.Namespace) -> None:
|
|
603
|
+
content = args.content
|
|
604
|
+
if content == "-":
|
|
605
|
+
content = sys.stdin.read()
|
|
606
|
+
if args.dry_run:
|
|
607
|
+
print(f"[dry-run] Would create paste '{args.title}' ({len(content)} chars)")
|
|
608
|
+
return
|
|
609
|
+
client = _get_client()
|
|
610
|
+
client.create_paste(args.title, content)
|
|
611
|
+
print(f"Paste '{args.title}' created.")
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def cmd_paste_delete(args: argparse.Namespace) -> None:
|
|
615
|
+
if args.dry_run:
|
|
616
|
+
print(f"[dry-run] Would delete paste: {args.title}")
|
|
617
|
+
return
|
|
618
|
+
client = _get_client()
|
|
619
|
+
print(client.delete_paste(args.title))
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
# ── Status commands ──────────────────────────────────────────────────────────
|
|
623
|
+
|
|
624
|
+
def cmd_status_post(args: argparse.Namespace) -> None:
|
|
625
|
+
if args.dry_run:
|
|
626
|
+
print(f"[dry-run] Would post status: {args.emoji} {args.content}")
|
|
627
|
+
return
|
|
628
|
+
client = _get_client()
|
|
629
|
+
client.post_status(args.content, emoji=args.emoji)
|
|
630
|
+
print("Status posted.")
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def cmd_status_list(args: argparse.Namespace) -> None:
|
|
634
|
+
client = _get_client()
|
|
635
|
+
for status in client.list_statuses():
|
|
636
|
+
print(f" {status.emoji} {status.content} ({status.created})")
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
# ── PURL commands ────────────────────────────────────────────────────────────
|
|
640
|
+
|
|
641
|
+
def cmd_purl_list(args: argparse.Namespace) -> None:
|
|
642
|
+
client = _get_client()
|
|
643
|
+
for purl in client.list_purls():
|
|
644
|
+
print(f" {purl.name:20s} → {purl.url}")
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def cmd_purl_create(args: argparse.Namespace) -> None:
|
|
648
|
+
if args.dry_run:
|
|
649
|
+
print(f"[dry-run] Would create PURL '{args.name}' → {args.url}")
|
|
650
|
+
return
|
|
651
|
+
client = _get_client()
|
|
652
|
+
client.create_purl(args.name, args.url, listed=not args.unlisted)
|
|
653
|
+
print(f"PURL '{args.name}' → {args.url}")
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def cmd_purl_delete(args: argparse.Namespace) -> None:
|
|
657
|
+
if args.dry_run:
|
|
658
|
+
print(f"[dry-run] Would delete PURL: {args.name}")
|
|
659
|
+
return
|
|
660
|
+
client = _get_client()
|
|
661
|
+
print(client.delete_purl(args.name))
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
# ── Now commands ─────────────────────────────────────────────────────────────
|
|
665
|
+
|
|
666
|
+
def cmd_now_get(args: argparse.Namespace) -> None:
|
|
667
|
+
client = _get_client()
|
|
668
|
+
now = client.get_now()
|
|
669
|
+
print(now.content)
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def cmd_now_update(args: argparse.Namespace) -> None:
|
|
673
|
+
content = args.content
|
|
674
|
+
if content == "-":
|
|
675
|
+
content = sys.stdin.read()
|
|
676
|
+
if args.dry_run:
|
|
677
|
+
print(f"[dry-run] Would update /now page ({len(content)} chars)")
|
|
678
|
+
return
|
|
679
|
+
client = _get_client()
|
|
680
|
+
client.update_now(content, listed=not args.unlisted)
|
|
681
|
+
print("Now page updated.")
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
# ── DNS commands ─────────────────────────────────────────────────────────────
|
|
685
|
+
|
|
686
|
+
def cmd_dns_list(args: argparse.Namespace) -> None:
|
|
687
|
+
client = _get_client()
|
|
688
|
+
for rec in client.list_dns_records():
|
|
689
|
+
print(f" [{rec.id}] {rec.record_type:6s} {rec.name:30s} → {rec.data} (TTL {rec.ttl})")
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def cmd_dns_create(args: argparse.Namespace) -> None:
|
|
693
|
+
if args.dry_run:
|
|
694
|
+
print(f"[dry-run] Would create DNS: {args.type} {args.name} → {args.data}")
|
|
695
|
+
return
|
|
696
|
+
client = _get_client()
|
|
697
|
+
rec = client.create_dns_record(args.type, args.name, args.data, ttl=args.ttl)
|
|
698
|
+
print(f"DNS record created: {rec.record_type} {rec.name} → {rec.data}")
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def cmd_dns_delete(args: argparse.Namespace) -> None:
|
|
702
|
+
if args.dry_run:
|
|
703
|
+
print(f"[dry-run] Would delete DNS record: {args.id}")
|
|
704
|
+
return
|
|
705
|
+
client = _get_client()
|
|
706
|
+
print(client.delete_dns_record(args.id))
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
# ── Web commands ─────────────────────────────────────────────────────────────
|
|
710
|
+
|
|
711
|
+
def cmd_web_get(args: argparse.Namespace) -> None:
|
|
712
|
+
client = _get_client()
|
|
713
|
+
web = client.get_web()
|
|
714
|
+
print(web.get("content", web))
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def cmd_web_update(args: argparse.Namespace) -> None:
|
|
718
|
+
content = args.content
|
|
719
|
+
if content == "-":
|
|
720
|
+
content = sys.stdin.read()
|
|
721
|
+
if args.dry_run:
|
|
722
|
+
print(f"[dry-run] Would update web page ({len(content)} chars, {'draft' if args.draft else 'publish'})")
|
|
723
|
+
return
|
|
724
|
+
client = _get_client()
|
|
725
|
+
client.update_web(content, publish=not args.draft)
|
|
726
|
+
print("Web page updated.")
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
# ── Email commands ───────────────────────────────────────────────────────────
|
|
730
|
+
|
|
731
|
+
def cmd_email_get(args: argparse.Namespace) -> None:
|
|
732
|
+
client = _get_client()
|
|
733
|
+
info = client.get_email_forwarding()
|
|
734
|
+
print(info)
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def cmd_email_set(args: argparse.Namespace) -> None:
|
|
738
|
+
if args.dry_run:
|
|
739
|
+
print(f"[dry-run] Would set email forwarding to: {args.destination}")
|
|
740
|
+
return
|
|
741
|
+
client = _get_client()
|
|
742
|
+
client.set_email_forwarding(args.destination)
|
|
743
|
+
print(f"Email forwarding set to {args.destination}")
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
# ── PFP command ──────────────────────────────────────────────────────────────
|
|
747
|
+
|
|
748
|
+
def cmd_pfp_upload(args: argparse.Namespace) -> None:
|
|
749
|
+
if args.dry_run:
|
|
750
|
+
print(f"[dry-run] Would upload profile picture: {args.file}")
|
|
751
|
+
return
|
|
752
|
+
client = _get_client()
|
|
753
|
+
print(client.upload_pfp(args.file))
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
# ── Info commands ────────────────────────────────────────────────────────────
|
|
757
|
+
|
|
758
|
+
def cmd_info(args: argparse.Namespace) -> None:
|
|
759
|
+
client = _get_client()
|
|
760
|
+
info = client.get_address_info()
|
|
761
|
+
print(info)
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
# ── Parser ───────────────────────────────────────────────────────────────────
|
|
765
|
+
|
|
766
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
767
|
+
parser = argparse.ArgumentParser(prog="omglol", description="omg.lol CLI client")
|
|
768
|
+
parser.add_argument("--dry-run", action="store_true", help="Preview what would be sent without making changes")
|
|
769
|
+
sub = parser.add_subparsers(dest="command")
|
|
770
|
+
|
|
771
|
+
# post (publish markdown files)
|
|
772
|
+
p = sub.add_parser("post", help="Publish markdown files to weblog")
|
|
773
|
+
p.add_argument("files", nargs="+", help="Markdown files to publish")
|
|
774
|
+
p.set_defaults(func=cmd_post)
|
|
775
|
+
|
|
776
|
+
# posts
|
|
777
|
+
posts = sub.add_parser("posts", help="Manage weblog posts")
|
|
778
|
+
posts_sub = posts.add_subparsers(dest="subcommand")
|
|
779
|
+
|
|
780
|
+
p = posts_sub.add_parser("list", help="List all posts")
|
|
781
|
+
p.set_defaults(func=cmd_posts_list)
|
|
782
|
+
|
|
783
|
+
p = posts_sub.add_parser("get", help="Get a post by ID")
|
|
784
|
+
p.add_argument("entry_id")
|
|
785
|
+
p.set_defaults(func=cmd_posts_get)
|
|
786
|
+
|
|
787
|
+
p = posts_sub.add_parser("delete", help="Delete a post")
|
|
788
|
+
p.add_argument("entry_id")
|
|
789
|
+
p.set_defaults(func=cmd_posts_delete)
|
|
790
|
+
|
|
791
|
+
# paste
|
|
792
|
+
paste = sub.add_parser("paste", help="Manage pastes")
|
|
793
|
+
paste_sub = paste.add_subparsers(dest="subcommand")
|
|
794
|
+
|
|
795
|
+
p = paste_sub.add_parser("list", help="List all pastes")
|
|
796
|
+
p.set_defaults(func=cmd_paste_list)
|
|
797
|
+
|
|
798
|
+
p = paste_sub.add_parser("get", help="Get a paste")
|
|
799
|
+
p.add_argument("title")
|
|
800
|
+
p.set_defaults(func=cmd_paste_get)
|
|
801
|
+
|
|
802
|
+
p = paste_sub.add_parser("create", help="Create a paste (use '-' for stdin)")
|
|
803
|
+
p.add_argument("title")
|
|
804
|
+
p.add_argument("content")
|
|
805
|
+
p.set_defaults(func=cmd_paste_create)
|
|
806
|
+
|
|
807
|
+
p = paste_sub.add_parser("delete", help="Delete a paste")
|
|
808
|
+
p.add_argument("title")
|
|
809
|
+
p.set_defaults(func=cmd_paste_delete)
|
|
810
|
+
|
|
811
|
+
# status
|
|
812
|
+
status = sub.add_parser("status", help="Manage statuslog")
|
|
813
|
+
status_sub = status.add_subparsers(dest="subcommand")
|
|
814
|
+
|
|
815
|
+
p = status_sub.add_parser("post", help="Post a status")
|
|
816
|
+
p.add_argument("content")
|
|
817
|
+
p.add_argument("--emoji", default="✍️")
|
|
818
|
+
p.set_defaults(func=cmd_status_post)
|
|
819
|
+
|
|
820
|
+
p = status_sub.add_parser("list", help="List all statuses")
|
|
821
|
+
p.set_defaults(func=cmd_status_list)
|
|
822
|
+
|
|
823
|
+
# purl
|
|
824
|
+
purl = sub.add_parser("purl", help="Manage PURLs")
|
|
825
|
+
purl_sub = purl.add_subparsers(dest="subcommand")
|
|
826
|
+
|
|
827
|
+
p = purl_sub.add_parser("list", help="List all PURLs")
|
|
828
|
+
p.set_defaults(func=cmd_purl_list)
|
|
829
|
+
|
|
830
|
+
p = purl_sub.add_parser("create", help="Create a PURL")
|
|
831
|
+
p.add_argument("name")
|
|
832
|
+
p.add_argument("url")
|
|
833
|
+
p.add_argument("--unlisted", action="store_true")
|
|
834
|
+
p.set_defaults(func=cmd_purl_create)
|
|
835
|
+
|
|
836
|
+
p = purl_sub.add_parser("delete", help="Delete a PURL")
|
|
837
|
+
p.add_argument("name")
|
|
838
|
+
p.set_defaults(func=cmd_purl_delete)
|
|
839
|
+
|
|
840
|
+
# now
|
|
841
|
+
now = sub.add_parser("now", help="Manage /now page")
|
|
842
|
+
now_sub = now.add_subparsers(dest="subcommand")
|
|
843
|
+
|
|
844
|
+
p = now_sub.add_parser("get", help="Show /now page")
|
|
845
|
+
p.set_defaults(func=cmd_now_get)
|
|
846
|
+
|
|
847
|
+
p = now_sub.add_parser("update", help="Update /now page (use '-' for stdin)")
|
|
848
|
+
p.add_argument("content")
|
|
849
|
+
p.add_argument("--unlisted", action="store_true")
|
|
850
|
+
p.set_defaults(func=cmd_now_update)
|
|
851
|
+
|
|
852
|
+
# dns
|
|
853
|
+
dns = sub.add_parser("dns", help="Manage DNS records")
|
|
854
|
+
dns_sub = dns.add_subparsers(dest="subcommand")
|
|
855
|
+
|
|
856
|
+
p = dns_sub.add_parser("list", help="List DNS records")
|
|
857
|
+
p.set_defaults(func=cmd_dns_list)
|
|
858
|
+
|
|
859
|
+
p = dns_sub.add_parser("create", help="Create a DNS record")
|
|
860
|
+
p.add_argument("type", choices=["A", "AAAA", "CNAME", "MX", "TXT", "SRV", "NS", "CAA"])
|
|
861
|
+
p.add_argument("name")
|
|
862
|
+
p.add_argument("data")
|
|
863
|
+
p.add_argument("--ttl", type=int, default=3600)
|
|
864
|
+
p.set_defaults(func=cmd_dns_create)
|
|
865
|
+
|
|
866
|
+
p = dns_sub.add_parser("delete", help="Delete a DNS record")
|
|
867
|
+
p.add_argument("id")
|
|
868
|
+
p.set_defaults(func=cmd_dns_delete)
|
|
869
|
+
|
|
870
|
+
# web
|
|
871
|
+
web = sub.add_parser("web", help="Manage web page / profile")
|
|
872
|
+
web_sub = web.add_subparsers(dest="subcommand")
|
|
873
|
+
|
|
874
|
+
p = web_sub.add_parser("get", help="Show web page content")
|
|
875
|
+
p.set_defaults(func=cmd_web_get)
|
|
876
|
+
|
|
877
|
+
p = web_sub.add_parser("update", help="Update web page (use '-' for stdin)")
|
|
878
|
+
p.add_argument("content")
|
|
879
|
+
p.add_argument("--draft", action="store_true", help="Save as draft without publishing")
|
|
880
|
+
p.set_defaults(func=cmd_web_update)
|
|
881
|
+
|
|
882
|
+
# email
|
|
883
|
+
email = sub.add_parser("email", help="Manage email forwarding")
|
|
884
|
+
email_sub = email.add_subparsers(dest="subcommand")
|
|
885
|
+
|
|
886
|
+
p = email_sub.add_parser("get", help="Show forwarding settings")
|
|
887
|
+
p.set_defaults(func=cmd_email_get)
|
|
888
|
+
|
|
889
|
+
p = email_sub.add_parser("set", help="Set forwarding destination")
|
|
890
|
+
p.add_argument("destination")
|
|
891
|
+
p.set_defaults(func=cmd_email_set)
|
|
892
|
+
|
|
893
|
+
# pfp
|
|
894
|
+
p = sub.add_parser("pfp", help="Upload profile picture")
|
|
895
|
+
p.add_argument("file", help="Image file to upload")
|
|
896
|
+
p.set_defaults(func=cmd_pfp_upload)
|
|
897
|
+
|
|
898
|
+
# info
|
|
899
|
+
p = sub.add_parser("info", help="Show address info")
|
|
900
|
+
p.set_defaults(func=cmd_info)
|
|
901
|
+
|
|
902
|
+
return parser
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def main():
|
|
906
|
+
parser = _build_parser()
|
|
907
|
+
args = parser.parse_args()
|
|
908
|
+
|
|
909
|
+
if not hasattr(args, "func"):
|
|
910
|
+
parser.print_help()
|
|
911
|
+
sys.exit(1)
|
|
912
|
+
|
|
913
|
+
args.func(args)
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
if __name__ == "__main__":
|
|
917
|
+
main()
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: omglol-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the omg.lol API — weblog, pastebin, PURLs, statuslog, DNS, and more
|
|
5
|
+
Project-URL: Homepage, https://github.com/dkd-dobberkau/omglol
|
|
6
|
+
Project-URL: Repository, https://github.com/dkd-dobberkau/omglol
|
|
7
|
+
Project-URL: Issues, https://github.com/dkd-dobberkau/omglol/issues
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: api-client,omg.lol,pastebin,statuslog,weblog
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
23
|
+
Requires-Dist: requests>=2.28.0
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# omglol
|
|
27
|
+
|
|
28
|
+
Python client and CLI for the [omg.lol](https://omg.lol) API — weblog, pastebin, statuslog, PURLs, DNS, /now page, and more.
|
|
29
|
+
|
|
30
|
+
## Setup
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
uv sync
|
|
34
|
+
cp .env.example .env # add your API key and address
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Get your API key at [home.omg.lol/account](https://home.omg.lol/account).
|
|
38
|
+
|
|
39
|
+
## CLI
|
|
40
|
+
|
|
41
|
+
After `uv sync`, the `omglol` command is available:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Publish markdown files to your weblog
|
|
45
|
+
omglol post article.md draft.md
|
|
46
|
+
|
|
47
|
+
# List weblog posts
|
|
48
|
+
omglol posts list
|
|
49
|
+
|
|
50
|
+
# Get a specific post
|
|
51
|
+
omglol posts get my-post-slug
|
|
52
|
+
|
|
53
|
+
# Delete a post
|
|
54
|
+
omglol posts delete my-post-slug
|
|
55
|
+
|
|
56
|
+
# Manage pastes
|
|
57
|
+
omglol paste list
|
|
58
|
+
omglol paste create my-note "Some content here"
|
|
59
|
+
omglol paste get my-note
|
|
60
|
+
omglol paste delete my-note
|
|
61
|
+
|
|
62
|
+
# Post and list statuses
|
|
63
|
+
omglol status post "Working on something cool"
|
|
64
|
+
omglol status post "Celebrating!" --emoji "🎉"
|
|
65
|
+
omglol status list
|
|
66
|
+
|
|
67
|
+
# Manage PURLs (persistent redirects)
|
|
68
|
+
omglol purl list
|
|
69
|
+
omglol purl create gh https://github.com/you
|
|
70
|
+
omglol purl delete gh
|
|
71
|
+
|
|
72
|
+
# /now page
|
|
73
|
+
omglol now get
|
|
74
|
+
omglol now update "Currently learning Rust"
|
|
75
|
+
|
|
76
|
+
# DNS records
|
|
77
|
+
omglol dns list
|
|
78
|
+
omglol dns create CNAME www example.com
|
|
79
|
+
omglol dns delete <record-id>
|
|
80
|
+
|
|
81
|
+
# Web page / profile
|
|
82
|
+
omglol web get
|
|
83
|
+
omglol web update "<h1>Hello</h1>"
|
|
84
|
+
omglol web update - < page.html # read from stdin
|
|
85
|
+
|
|
86
|
+
# Email forwarding
|
|
87
|
+
omglol email get
|
|
88
|
+
omglol email set me@example.com
|
|
89
|
+
|
|
90
|
+
# Profile picture
|
|
91
|
+
omglol pfp avatar.png
|
|
92
|
+
|
|
93
|
+
# Account info
|
|
94
|
+
omglol info
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Dry run
|
|
98
|
+
|
|
99
|
+
Preview what would be sent without making changes:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
omglol --dry-run post article.md
|
|
103
|
+
omglol --dry-run status post "Test"
|
|
104
|
+
omglol --dry-run dns create A @ 1.2.3.4
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Library
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from omglol import OmgLol
|
|
111
|
+
|
|
112
|
+
client = OmgLol(api_key="your_key", address="yourname")
|
|
113
|
+
|
|
114
|
+
# Weblog
|
|
115
|
+
client.post_markdown("my-post.md")
|
|
116
|
+
client.create_post(title="Hello World", content="This is my post.")
|
|
117
|
+
for post in client.list_posts():
|
|
118
|
+
print(post.title, post.location)
|
|
119
|
+
client.delete_post("my-post-slug")
|
|
120
|
+
|
|
121
|
+
# Pastebin
|
|
122
|
+
client.create_paste("notes", "Some text")
|
|
123
|
+
for paste in client.list_pastes():
|
|
124
|
+
print(paste.title)
|
|
125
|
+
|
|
126
|
+
# Statuslog
|
|
127
|
+
client.post_status("Hello from the API!", emoji="🎉")
|
|
128
|
+
for status in client.list_statuses():
|
|
129
|
+
print(status.emoji, status.content)
|
|
130
|
+
|
|
131
|
+
# PURLs
|
|
132
|
+
client.create_purl("gh", "https://github.com/you")
|
|
133
|
+
for purl in client.list_purls():
|
|
134
|
+
print(f"{purl.name} → {purl.url}")
|
|
135
|
+
|
|
136
|
+
# /now page
|
|
137
|
+
now = client.get_now()
|
|
138
|
+
print(now.content)
|
|
139
|
+
client.update_now("Currently building things")
|
|
140
|
+
|
|
141
|
+
# DNS
|
|
142
|
+
for rec in client.list_dns_records():
|
|
143
|
+
print(f"{rec.record_type} {rec.name} → {rec.data}")
|
|
144
|
+
client.create_dns_record("CNAME", "www", "example.com")
|
|
145
|
+
|
|
146
|
+
# Web page / profile
|
|
147
|
+
client.update_web("<h1>My page</h1>")
|
|
148
|
+
|
|
149
|
+
# Email forwarding
|
|
150
|
+
client.set_email_forwarding("me@example.com")
|
|
151
|
+
|
|
152
|
+
# Profile picture
|
|
153
|
+
client.upload_pfp("avatar.png")
|
|
154
|
+
|
|
155
|
+
# Account
|
|
156
|
+
info = client.get_address_info()
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
All list/get methods return typed dataclasses (`Post`, `Paste`, `Status`, `Purl`, `DnsRecord`, `NowPage`) instead of raw dicts.
|
|
160
|
+
|
|
161
|
+
### Markdown frontmatter
|
|
162
|
+
|
|
163
|
+
Weblog posts can include optional frontmatter:
|
|
164
|
+
|
|
165
|
+
```markdown
|
|
166
|
+
---
|
|
167
|
+
title: My Post Title
|
|
168
|
+
date: 2026-03-14 12:00
|
|
169
|
+
slug: custom-slug
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
Body content here...
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
If no frontmatter is present, the first `# Heading` is used as the title.
|
|
176
|
+
|
|
177
|
+
## Environment variables
|
|
178
|
+
|
|
179
|
+
| Variable | Description |
|
|
180
|
+
|---|---|
|
|
181
|
+
| `OMGLOL_API_KEY` | Your omg.lol API key |
|
|
182
|
+
| `OMGLOL_ADDRESS` | Your omg.lol address (e.g. `olivier`) |
|
|
183
|
+
|
|
184
|
+
## Development
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
uv sync
|
|
188
|
+
uv run pytest -v
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
MIT
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
omglol.py,sha256=XZ68kvoxXQ5QpQCUiEocZ83HzT80eaodiczE1hkloiY,33608
|
|
2
|
+
omglol_api-0.1.0.dist-info/METADATA,sha256=Ax8B0lq8QAHn-1P8MnP5yOPRBuOOv4WOXh0hvj1pPN4,4477
|
|
3
|
+
omglol_api-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
4
|
+
omglol_api-0.1.0.dist-info/entry_points.txt,sha256=fm6OGIGfIm0Xh29mK046UoiTknCp0ZNozz_L7RvArJ8,39
|
|
5
|
+
omglol_api-0.1.0.dist-info/licenses/LICENSE,sha256=JayWs0Ytc8fmxxCU2T1-iGSDC1N0fVEoSe02apJqj30,1074
|
|
6
|
+
omglol_api-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Olivier Dobberkau
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|