Compare commits

..

9 commits

Author SHA1 Message Date
824a439158 🔖 release 0.5.0
All checks were successful
Release / check_for_changed_version (push) Successful in 29s
Release / build (push) Successful in 7m34s
2025-09-18 11:57:39 -04:00
7943467f1c add wordle
Some checks failed
Release / check_for_changed_version (push) Has been cancelled
Release / build (push) Has been cancelled
2025-09-18 11:56:58 -04:00
55c3307b74 🔖 Release 0.4.0
All checks were successful
Release / check_for_changed_version (push) Successful in 31s
Release / build (push) Successful in 6m47s
2025-09-16 13:55:27 -04:00
f54c0af88c Add missing dependencies
Some checks are pending
Release / check_for_changed_version (push) Waiting to run
Release / build (push) Waiting to run
2025-09-16 13:54:49 -04:00
e27edba78e ⬆️ Update dependencies and poetry version
Some checks failed
Release / check_for_changed_version (push) Successful in 32s
Release / build (push) Has been cancelled
2025-09-16 13:49:44 -04:00
8ab616e5fd 🚀 It's a new version!
All checks were successful
Release / check_for_changed_version (push) Successful in 26s
Release / build (push) Successful in 3m53s
2023-05-19 09:52:57 -04:00
1749a8659a Merge pull request 'reddit-support' (#1) from reddit-support into master
All checks were successful
Release / check_for_changed_version (push) Successful in 31s
Release / build (push) Successful in 4m10s
Reviewed-on: #1
2023-05-18 21:51:39 -04:00
5592c0e7cd finish reddit support 2023-05-18 21:49:23 -04:00
b8a482f86c 👷 doin' the thing 2023-05-18 18:45:40 -04:00
6 changed files with 796 additions and 825 deletions

3
.gitignore vendored
View file

@ -128,4 +128,5 @@ dmypy.json
# Pyre type checker
.pyre/
setup.py
utils
utils
praw.ini

1313
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "src"
version = "0.2.2"
version = "0.5.0"
description = ""
authors = ["Joe Kaufeld <opensource@joekaufeld.com>"]
@ -11,9 +11,12 @@ httpx = "^0.23.0"
click = "^8.1.3"
rich = "^12.5.1"
markdownify = "^0.11.6"
praw = "^7.7.0"
html5lib = "^1.1"
lxml = "^6.0.1"
[tool.poetry.dev-dependencies]
poetry = "^1.1.14"
[poetry.group.dev.dependencies]
poetry = "^2"
black = "^22.6.0"
[build-system]

View file

@ -6,14 +6,16 @@ import uuid
import click
import httpx
from praw import Reddit
from rich import pretty
from rich.status import Status
from rich.traceback import install
from shiv.bootstrap import current_zipfile
import src
from src.helpers import flip_char
from src.joplin import process_joplin_posts
from src.helpers import flip_char, load_config, write_config
from src.joplin import process_joplin_posts, get_folders, BASE_URL
from src.wordle import wordle
from src.art import BANNERS
@ -26,12 +28,12 @@ from src.art import BANNERS
@click.option(
"--update",
is_flag=True,
help="Check Gitea for a new version and auto-update.",
help="Check Forgejo for a new version and auto-update.",
)
def main(ctx, update):
"""Launch a utility or drop into a command line REPL if no command is given."""
if update:
update_from_gitea()
update_from_forgejo()
sys.exit()
elif ctx.invoked_subcommand is None:
banner = random.choice(BANNERS)
@ -115,8 +117,100 @@ def beautify(words: list[str]):
click.echo("".join(new_beautiful_string))
def update_from_gitea():
"""Get the newest release from Gitea and install it."""
@main.command()
def get_saved_from_reddit() -> None:
"""Get saved posts from reddit."""
def _process(item):
fullname = item.fullname
if fullname.startswith("t3"):
# we got a post
title = f"{item.subreddit.display_name} - {item.title}"
body = item.selftext if item.selftext else item.url
elif fullname.startswith("t1"):
# comment time
try:
author_name = item.author.name
except AttributeError:
author_name = "[deleted]"
title = (
f"{item.submission.subreddit.display_name} - {item.submission.title}"
)
body = f"Comment from {author_name}:\n\n{item.body}"
else:
click.echo(f"Not sure how to process https://reddit.com{item.permalink}")
return
if item.over_18:
title = "🔴 " + title
body = "https://reddit.com" + item.permalink + "\n\n" + body
joplin = BASE_URL + cfg["JOPLIN_PORT"]
notes_url = joplin + f"/notes/?token={cfg.get('JOPLIN_TOKEN')}"
click.echo(f"Processing {title}...")
httpx.post(
notes_url,
json={
"title": title,
"body": body,
"parent_id": cfg.get("joplin_saved_posts_folder_id"),
},
)
item.unsave()
cfg = load_config()
# because 2fa is enabled, we have to get the code first
twofa_code = click.prompt("What's the current 2fa code?", type=str)
if not cfg.get("REDDIT_CLIENT_ID"):
cfg["REDDIT_CLIENT_ID"] = ""
if not cfg.get("REDDIT_CLIENT_SECRET"):
cfg["REDDIT_CLIENT_SECRET"] = ""
if not cfg.get("REDDIT_PASSWORD"):
cfg["REDDIT_PASSWORD"] = ""
if not cfg.get("REDDIT_USERNAME"):
cfg["REDDIT_USERNAME"] = ""
if not cfg.get("joplin_saved_posts_folder_id"):
cfg["joplin_saved_posts_folder_id"] = ""
write_config(cfg)
username = cfg.get("REDDIT_USERNAME", "")
r = Reddit(
client_id=cfg.get("REDDIT_CLIENT_ID", ""),
client_secret=cfg.get("REDDIT_CLIENT_SECRET", ""),
username=username,
password=f"{cfg.get('REDDIT_PASSWORD', '')}:{twofa_code}",
user_agent=f"getter of saved posts, u/{username}",
)
try:
r.user.me()
except Exception as e:
click.echo(f"Cannot connect to reddit: {e}")
return
if not cfg.get("joplin_saved_posts_folder_id"):
folders = get_folders()
click.echo("Pick the folder that I should write your saved posts to:")
for count, option in enumerate(folders["items"]):
click.echo(f"{count} - {option['title']}")
folder_position = click.prompt("Number of folder?", type=int)
folder_name = folders["items"][folder_position]["title"]
folder_id = folders["items"][folder_position]["id"]
click.echo(f"Got it, will write to {folder_name}, ID {folder_id}.")
cfg["joplin_saved_posts_folder_id"] = folder_id
write_config(cfg)
for _ in range(10):
click.echo("Getting new posts...")
for item in r.user.me().saved(limit=None):
_process(item)
def update_from_forgejo():
"""Get the newest release from Forgejo and install it."""
status = Status("Checking for new release...")
status.start()
@ -126,7 +220,7 @@ def update_from_gitea():
if response.status_code != 200:
status.stop()
click.echo(
f"Something went wrong when talking to Gitea; got a"
f"Something went wrong when talking to Forgejo; got a"
f" {response.status_code} with the following content:\n"
f"{response.content}"
)
@ -150,6 +244,7 @@ def update_from_gitea():
status.stop()
click.echo(f"Updated to {release_data['tag_name']}! 🎉")
main.add_command(wordle)
if __name__ == "__main__":
main()

View file

@ -105,18 +105,20 @@ def process_joplin_posts():
httpx.get(
joplin
+ f"/notes/{blob['id']}?fields=body&token={c.get('JOPLIN_TOKEN')}"
).json()['body'].strip()
)
.json()["body"]
.strip()
)
click.echo(f"Processing {url}...")
site = httpx.get(url, follow_redirects=True)
soup = bs4.BeautifulSoup(site.content, features="html5lib")
# clean up that schizz
title = soup.title.text
[t.extract() for t in soup(['script', 'head', 'style'])]
[t.extract() for t in soup(["script", "head", "style"])]
body = md(str(soup))
httpx.put(
joplin + f"/notes/{blob['id']}?token={c.get('JOPLIN_TOKEN')}",
json={'title': title, 'body': body}
json={"title": title, "body": body},
)
click.echo(resp)

179
src/wordle.py Normal file
View file

@ -0,0 +1,179 @@
import random
import click
import httpx
from rich.status import Status
from src.helpers import base_folder
all_valid_guesses = "https://gist.githubusercontent.com/itsthejoker/7d4c0d10a4e97cc7493e765d9e848a53/raw/b3970cc79bf88c872dd67c3896d8a8673377b0fd/wordle-nyt-allowed-guesses-update-12546.txt"
all_answers = "https://gist.githubusercontent.com/itsthejoker/1c7b8b97c454ce464aa5ea7f187d81e9/raw/1a2e59929b833925af800298d1003e48d90b5898/wordle-nyt-answers-alphabetical.txt"
answers_path = base_folder / "opsbox_wordle_answers.txt"
guesses_path = base_folder / "opsbox_wordle_guesses.txt"
def download_wordle_data():
with httpx.Client() as client:
with open(answers_path, "w") as f:
f.write(client.get(all_answers).text)
with open(guesses_path, "w") as f:
f.write(client.get(all_valid_guesses).text)
def check_if_need_to_download_data():
if not answers_path.exists() or not guesses_path.exists():
download_wordle_data()
def load_data():
status = Status("Setting up game...")
status.start()
check_if_need_to_download_data()
with open(answers_path, "r") as f:
answers = f.read().splitlines()
with open(guesses_path, "r") as f:
guesses = f.read().splitlines()
guesses += answers # answers are valid guesses
status.stop()
return answers, guesses
def print_keyboard(guessed_word_list: list[str], keyboard_display: dict[str, int]):
first_row = "qwertyuiop"
second_row = "asdfghjkl"
third_row = "zxcvbnm"
click.echo()
all_letters_guessed = set("".join(guessed_word_list))
for count, item in enumerate([first_row, second_row, third_row]):
click.echo(" " * count if count != 2 else " " * 3, nl=False)
for letter in item:
click.echo(" ", nl=False)
if letter in all_letters_guessed and keyboard_display.get(letter) == 2:
click.echo(
click.style(letter.capitalize(), fg="green", bold=True), nl=False
)
elif letter in all_letters_guessed and keyboard_display.get(letter) == 1:
click.echo(
click.style(letter.capitalize(), fg="yellow", bold=True), nl=False
)
elif letter in all_letters_guessed:
click.echo(
click.style(letter.capitalize(), fg="white", bold=True), nl=False
)
else:
click.echo(click.style(letter.capitalize(), fg="white"), nl=False)
click.echo("\n")
def print_word_matrix(
answer: str,
letter_counts: dict[str, int],
guessed_word_list: list[str],
keyboard_display: dict[str, int],
) -> None:
for guess in guessed_word_list:
guess_letter_counts = {}
click.echo(" ", nl=False)
for count, letter in enumerate(guess):
add_newline = count == 4
guess_letter_counts[letter] = guess_letter_counts.get(letter, 0) + 1
if letter == answer[count]:
click.echo(
click.style(letter.capitalize(), fg="green", bold=True),
nl=add_newline,
)
keyboard_display[letter] = 2
elif (
letter in answer
and guess_letter_counts[letter] <= letter_counts[letter]
):
click.echo(
click.style(letter.capitalize(), fg="yellow", bold=True),
nl=add_newline,
)
if keyboard_display.get(letter) != 2:
keyboard_display[letter] = 1
elif (
letter in answer and guess_letter_counts[letter] > letter_counts[letter]
):
click.echo(
click.style(letter.capitalize(), fg="white", bold=True),
nl=add_newline,
)
else:
click.echo(
click.style(letter.capitalize(), fg="white", bold=True),
nl=add_newline,
)
if len(guessed_word_list) < 6 and answer not in guessed_word_list:
click.echo()
click.echo("You have ", nl=False)
click.echo(
click.style(str(6 - len(guessed_word_list)), fg="white", bold=True),
nl=False,
)
click.echo(f" guess{'es' if len(guessed_word_list) != 4 else ''} left.")
def get_unique(sequence):
seen = set()
return [x for x in sequence if not (x in seen or seen.add(x))]
@click.command(help="Start a new game of Wordle!")
@click.option("--debug", is_flag=True, help="Debug mode")
def wordle(debug):
click.echo()
click.echo("Starting a new game of Wordle!")
click.echo("You have six tries to find a 5 letter word.")
click.echo(
"Gray letters are not in the word, yellow letters are in the word but in the"
" wrong place, and green letters are in the right place."
)
click.echo("Good luck!")
click.echo()
answers, guesses = load_data()
todays_word = random.choice(answers)
# every key is a letter, the value is 1 if it's yellow and 2 if it's green
keyboard_display = {}
if debug:
click.echo(f"Today's word is {todays_word}")
guessed_words = []
letter_set = get_unique(todays_word)
letter_counts = {letter: todays_word.count(letter) for letter in letter_set}
while True:
if len(guessed_words) == 6:
click.echo("You lose!")
click.echo("The word was: ", nl=False)
click.echo(click.style(str(6 - len(todays_word)), fg="white", bold=True))
click.echo("Better luck next time!")
break
guess = click.prompt("Enter a guess")
guess = guess.lower()
click.echo()
if len(guess) < 5 or len(guess) > 5:
click.echo("Guesses must be 5 letters.")
continue
if guess not in guesses:
click.echo("Sorry, that's not a valid guess. Try again!")
continue
guessed_words += [guess]
print_word_matrix(todays_word, letter_counts, guessed_words, keyboard_display)
print_keyboard(guessed_words, keyboard_display)
click.echo()
if guess == todays_word:
click.echo("You win!")
break