{'title': 'archyvas', 'date': '2020-07-01', 'tags': 'exception.lt', 'type': 'post'}


#emojipher #dontdothisplease #whathaveidone

How to use Emojipher to encrypt the Holy Bible:

# chmod +x emojipher!
$ chmod +x emojipher.py

# Generate a key.
$ ./emojipher -g key

# Get a bible?

# Encrypt it.
$ ./emojipher -e the_holy_bible.pdf -k key.emojipher

# Enjoy it.

Wanna see the encrypted bible? Click here.



This site finally builds valid HTML from Markdown, including copying media to the right directory! It only took me weeks of brain computing power and eye damage staring at my code to figure it all out. Did you know that Python os library cannot properly join relative and absolute paths together? You will need pathlib for that:

from pathlib import Path

# You can give Path an absolute and relative paths, and it will combine them without a problem!
full_path = Path(cool_path, some_other_path)
full_path = p.resolve()

What does my page builder do as of today?

  1. Convert Markdown to HTML with Python Markdown2.
  2. Create list of contents from headings, if such option is selected.
  3. Copy images, videos to media directory of the website.
  4. Build HTML page using templates.
  5. Colorize code with Pygments stylesheet.
  6. Creates a backup of the whole website before building a new version.

What is left to do:

  1. Minify and inline stylesheets into HTML.
  2. Minify media automatically when building.
  3. Remove hard-coded solutions to make it work on different directory structure/website.
  4. GitHub action to build website on master branch update.
  5. GitHub action to send newest backup to my archive on rpi0.
  6. Add a guestbook d(-_-)b


#python #emojipher #cryptography #verybadcryptograhpy

Been a while since I littered this place with useless info. Here we go again…

Today I have decided to finally accomplish one of my old ideas - encryption algorithm that encrypts data by masking it with emojis. Yup, emojis. It is pretty simple, a symmetric key is a dictionary of byte values matched to a list of random emojis. A list is 3 to 7 emojis long and contains unique random emojis from ~1300 available emojis that span one Unicode character. Otherwise, you cannot have one long string of emojis without storing additional information to distinguish between emojis of 1 Unicode and 5 Unicode or even 7 Unicode combinations. Frequency analysis weakness of ancient algorithms like Vigenére cipher should not be a problem. What problems does this algorithm have? Well… it increases the size of encrypted load by 10 times. Perfect encryption to generate useless data ;P

Want to try it out? Heres the sauce:

#!/usr/bin/env python3

from argparse import ArgumentParser, RawDescriptionHelpFormatter
from emojipedia import Emojipedia
import secrets
import os.path
import ast

def get_all_emojis() -> set:
    """Uses Emojipedia module to get all existing emojis as a set.
    This takes a while. And uses a lot of memory. Should be rewritten.
    return set([x.character for x in Emojipedia.all() if len(str(x.character)) == 1])

def get_random_emoji_list(emoji_list: set) -> list:
    """Generates and returns a list of random emojis from a list of all available emojis.
    Uses secure random generator https://docs.python.org/3.6/library/secrets.html
    return [emoji_list.pop() for x in range(secrets.randbelow(4) + 3)]

def generate_key(filename):
    """Generates a key in form of a dictionary byte:emoji_list.
    Stores key as key.emojipher. Overwrites existing key if exists!

    1. Generate a list of all emojis.
    2. Generate a dictionary of bytes 0 - 256 and their list of emoji.
    3. Store key as key.emojipher.

    print("👁👁 Generating key, I am not looking...")

    emojis = get_all_emojis()

    key = {x : get_random_emoji_list(emojis) for x in range(0, 256)}

    with open(filename + ".emojipher", "w") as f:
        # Write as string, can evaluate string to dict when reading key.

    print("🧑‍💻🤐 Key has been generated! Keep it safe.")

def encrypt_data(filename: str, key: str):
    """Encrypts given file with given key. If such file exists, it is deleted!

    1. Load key.
    2. Iterate file bytes.
    2.1. Convert byte to int and find matching key in dict.
    2.2. Pick one random emoji from according list,
    2.3. Append to file. 

    print("🧙🏼🔥 Encrypting your data...")

    with open(key, "r") as fr:
        key = ast.literal_eval(fr.read())

    output_filename = filename + ".encrypted"

    # Remove existing file if needed.
    if os.path.exists(output_filename):

    with open(filename, "rb") as fr:
        byte = fr.read(1)
        while byte != b"":
            # Convert byte to int to match in key dictionary.
            b = int.from_bytes(byte, "little")
            byte = fr.read(1)

            with open(output_filename, "a") as fw:
                # Find matching random emoji and write to file.

    print("🕵 🕵️‍♂️ Your data has been encrypted and is safe from bioluminescent agents.")

def decrypt_data(filename: str, key: str):
    """Given a filename and a key, decrypts data into original file."""

    print("👀👽 Decrypting data, make sure you are alone!")

    with open(key, "r") as fr:
        key = ast.literal_eval(fr.read())

    # Invert key, aka make each emoji in list of each key into a new key.
    # So one key with list of 5 emojis becomes 5 keys with same value of byte.
    inverted_key = {}
    for k, v in key.items():
        for e in v:
            inverted_key[e] = k.to_bytes(1, "little")

    output_filename = filename.replace(".emojipher", "")

    if os.path.exists(output_filename):

    with open(filename, "r", encoding="utf-8") as fr:
        while True:
            c = fr.read(1)
            if not c:

            with open(output_filename, "ab") as fw:

    print("👨🏻‍💻🖨 Your data has been decrypted. Have fun!")

if __name__ == "__main__":
    description = """

    Your favorite encryption tool!

    Are you bored of cryptographically safe means of encryption like AES256?
    You hate asymmetric encryption?
    You are a modern human that loves big hard drives and emojis?
    You want to waste as much bandwith as you can?
    You want to choke NSA and TENCENT servers with emoji?

    If you answered any of those questions with YES, this script is for you.

    Encrypt your data by turning each byte of source file into emoji! Yes, thats right.
    Each byte becomes 4 bytes! And you get multiple emoji possibilities per byte!
    Frequency analysis? More like waste of electricity.

    Okay, I want this, but how do I start?

    1. Generate your personal key by issuing. It will append .emojipher to the end of your given name.
        $ ./emojipher.py -g my_key

    2. Encrypt some data!
        $ ./emojipher.py -e my_secrets.txt -k my_key.emojipher

    3. ... Do whatever you want, store it, send it, share it 🤡

    4. Decrypt your precious data.
        $ ./emojipher.py -d my_secrets.txt.encrypted -k my_key.emojipher

    5. Enjoy your decrypted data, but make sure you are alone.

    Uses https://github.com/bcongdon/python-emojipedia for emoji generation 😍

    Made with 🤡 by the 🤤 in www.exception.lt

    parser = ArgumentParser(description=description, formatter_class=RawDescriptionHelpFormatter)
    generate = parser.add_argument_group("key generation")
    generate.add_argument("-g", "--generate", type=str, help="key name")
    group = parser.add_mutually_exclusive_group()
    group.add_argument("-e", "--encrypt", type=str, help="file(s) to encrypt")
    group.add_argument("-d", "--decrypt", type=str, help="file(s) to decrypt")
    parser.add_argument("-k", "--key", type=str, help="path to key")
    args = parser.parse_args()

    if args.generate:

    elif args.encrypt:
        encrypt_data(args.encrypt, args.key)

    elif args.decrypt:
        decrypt_data(args.decrypt, args.key)

And here is how an encrypted PGP key looks like:

gedit screenshot of a pgp key with emojipher encryption


#learning #procrastination #todo #bookmarked

Do you have problems with doing stuff that needs to be done? I do. Very much. So I compiled you a bulletproof list of lists. It is meant to slay all procrastination there is. Can’t read it now? Bookmark it. Not in the mood? Subscribe to my daily newsletter and you will get parts of this list daily. Add your phone number, I will call you.

  1. Discipline over motivation.
  2. Massive projects should be broken down into smaller tasks until the task is manageable.
  3. Wake up at 05:00. Go for a jog.
  4. Decide on tasks that you will do at the start of the day. Have a fixed timeline, otherwise, you won’t accomplish as much.
  5. Have fixed working time in a working room, no fun is allowed in that room.
  6. Subscribe to 10 newsletters that will send you productivity tips.
  7. Delete all incoming emails if you do not have time to reply to them.
  8. Do not work in free time, have work and free time that are separate to train your brain like a dog - to behave…
  9. Do journaling lmao.
  10. Categorize shit into levels: asap, important can wait.
  11. Call your mom daily.
  12. Meditate and do yoga.
  13. Read self-help books and Thinking Fast & Slow.
  14. If you can do it in under 2 minutes, do it directly.
  15. Read motivational quotes.
  16. Use Pomodoro, but with longer timers. To not break your programming flow.
  17. Do not cram, use ANKI, or whatever.
  18. Brush your teeth and wipe your ass properly. Do not use your spy-phone when you do either of those.
  19. Get a personal driver and a maid. Chef if you make Silicon Valley cash for adjusting fonts all day long.
  20. Block social media and other garbage sites. Nobody cares about you, and you should not care about useless information.

That’s it. Thank you. You can also use a shorter list:

  1. Do stuff now.
  2. Wake up early.
  3. Don’t don’t do stuff.
  4. Use less phone, use more brain.

homer taking notes of my list


Found a cool command to see your top 10 bash commands from history. Source. Type the command below and look at the stuff you should create aliases for.

$ history | awk '{CMD[$2]++;count++;}END { for (a in CMD)print CMD[a] " " CMD[a]/count*100 "% " a;}' | grep -v "./" | column -c3 -s " " -t | sort -nr | nl | head -n10

It will look something like this:

     1    181  18.1%  python3
     2    88   8.8%   git
     3    80   8%     sudo
     4    76   7.6%   cd
     5    70   7%     touch
     6    58   5.8%   nano
     7    38   3.8%   ls
     8    34   3.4%   pip3
     9    26   2.6%   exit
    10    22   2.2%   youtube-dl

And hopefully not like this:

     1    9999  100.0%  cowsay


#python #scriptlet

Oh my, you wrote a file using wrong headings? Did you get bullied because of this?

Don’t worry. Exception.lt is here to save your ass. This small script will push down all headings by one. It can probably be replaced with one line of grep|sed, but it takes longer to figure that out than to glue together a Python script. Did you make a mistake and want to push your headings around? This script can do it.

#!/usr/bin/env python3

import re
from argparse import ArgumentParser, RawDescriptionHelpFormatter

def move_headings(file_in, file_out, amount):
    hashes = amount * "#"
    matches = []

    def _extend(m):
        # [:-1] to remove whitespace before adding new # and whitespace.
        return f"{m.group()[:-1]}{hashes} " if amount >= 0 else f"{m.group()[:-1 + amount]} "

    pattern = re.compile("#{1,6} ")

    with open(file_in) as f:
        c = f.read()
        c = re.sub(pattern, _extend, c)

        with open(file_out, "w") as fw:

if __name__ == "__main__":
    description = """


    Takes input file, output path and amount of #'s to push either up or down.
    There are no limit to how much you can push to either side. Have fun!


    Push down by one:

        moveh.py -i sql.md -o sql2.md -a 1

    Push back by two:

        moveh.py -i oof.md -o oof.md -a -2

    Push down by crash:

        moveh.py -i t.md -o t2.md -a 9999999999999999999

    Author: The Truth from San Andreas
    Date: 2020-06-23
    parser = ArgumentParser(description=description, formatter_class=RawDescriptionHelpFormatter)
    parser.add_argument("-i", "--input", type=str, help="path to markdown file.", required=True)
    parser.add_argument("-o", "--output", type=str, help="path to output file.", required=True)
    parser.add_argument("-a", "--amount", type=int, help="how many to push down or up", required=True)
    args = parser.parse_args()
    move_headings(args.input, args.output, args.amount)


#lastfm #python

I’m still on my journey to extract all of my (useful) data from various online services. After figuring out generous Wakatime API, I went on to exploit Last.fm. There is a bit more data, but they also provide a simple API. Not going to bother you like the last time, take a peek at the code yourself. Yes, it is one massive function that goes against every single page of the Clean Code, but it is easy to follow in a post. The production version looks better. What’s next? Already working on Telegram, Firefox and Todoist data extraction.

import requests
import datetime
import sqlite3
import pytz

def parse_scrobbles(database_path, api_key, username):
    # First response to extract number of pages for iteration.
    url = f"""http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks
    response = requests.get(url)
    response_json = response.json()

    # Get the amount of pages to iterate.
    pages = response_json['recenttracks']['@attr']['totalPages']

    with sqlite3.connect(database_path) as cn:
        c = cn.cursor()
        # Date is primary key, since you cannot listen to two songs at once, unless you account is stolen.
        c.execute("""CREATE TABLE IF NOT EXISTS lastfm_scrobbles (
                     date TEXT PRIMARY KEY, 
                     track TEXT, 
                     artist TEXT, 
                     album TEXT

        # If there have been no previous entries, set it to None.
            # Get the newest scrobble from the database.
            c.execute("SELECT date FROM lastfm_scrobbles ORDER BY date DESC LIMIT 1")
            newest_scrobble = c.fetchone()[0]
        except TypeError:
            newest_scrobble = None

        scrobbles = 0

        for page in range(1, int(pages) + 1):
            url = f"""http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks
            response = requests.get(url)
            response_json = response.json()["recenttracks"]["track"]

            for track in response_json:
                # Convert to UTC.
                dt = datetime.datetime.fromtimestamp(int(track['date']['uts']))
                dt = dt.astimezone(pytz.utc)
                dt = dt.isoformat()

                values = (dt, track['name'], track['artist']['#text'], track['album']['#text'])
                c.execute("INSERT OR IGNORE INTO lastfm_scrobbles VALUES (?, ?, ?, ?)", values)

                if newest_scrobble is not None and newest_scrobble == dt:
                    return scrobbles

                scrobbles += 1

        return scrobbles

if __name__ == "__main__":
    api_key = ""
    username = ""
    database_path = ""
    scrobbles = parse_scrobbles(database_path=database_path, api_key=api_key, username=username)
    print(f"added {scrobbles} new scrobbles to the database!")



I made a pizza after watching a video about how to make one at 3 am. Here is the original video. If you do not speak german, I made you a tutorial:

Things you will need:

What to do:

  1. Mix flour, water, yeast, salt together. Use your hands and give it a proper mix. Knead it, twist it for at least 5 minutes. It has to be stretchy and should not tear. This is the only hard part. Spend some time making good stuff, otherwise, just eat frozen pizza and don’t bother.
  2. Leave it in the fridge for the night. Add some olive oil to lube it up.
  3. Cut your dough in 3-4 parts. Use one ball, put the remaining balls back and make some more later.
  4. Start heating your oven. Use top heating only. You will have to place the pizza near the top.
  5. Make a pizza? Roll out or twist the dough until you have a pizza-shaped pad. Make it thin.
  6. Take a pan that fits this pad, sprinkle some flour in the pan.
  7. Put your pad in the pan, start heating it. Make some adjustments, roll up the crust on the sides.
  8. Add sauce, cheese, all of your other toppings.
  9. Wait until the bottom is crusty.
  10. Yoink out the pizza and yeet it in the oven. Not with the pan. Just the pizza.
  11. Stare at it until it is crusty enough for you. No exact time, all cheap ovens are shit and their temperatures and baking times are random. It should take between 3 and 888 minutes, depending on the heat of your oven. Don’t burn down your house.
  12. Eat your pizza. Add some fresh basil from your eco gentrified balcony greens garden.

how the pizza looks like


#wakatime #seo #python

I have been thinking about this page. I know that bots are here to scrape data and index it, I know that humans are motivated by selfish needs - they want to consume useful-to-them-information all day long. So here you go. I was bothered by Wakatime. They only offer 7 days of graphs with their free service, can it get any worse? Well, at least there is a very well made API. So I spent a part of my day to write a script that retrieves that data, takes out the good parts and stores them in a database. Now I could talk about some affiliate links, services I recommend, give you a story to boost my SEO.

Did you know that Python is named after a show Monty Python? Good for you, because I know that Python is called this way, so now you also know why Python is called Python. This post is now also an SEO tutorial. Repeating keywords will still boost your page a bit, not as it used to when you could just spam Python ten thousand times and get the best rating there is until someone would spam Python one hundred thousand times. Search engines are a bit better now in this regard. But they did sure went to shit when it comes to useful information. Have you noticed that most of the content you search for on Google is just adware or SEO boosted content that is trash quality? I did. I hate it. If you are one of the two humans who read this text - congratulations, your attention span is above average. Microsoft did a thing and came up with a number: most of the braindead information consumers can only focus on one thing for 8 seconds.

That’s it.

Oh, I almost forgot. You should make the script run in your crontab to get the new data. I run it daily with: 0 0 * * * /home/null/scripts/wakatime.py.

Now go look at the actual code:


import requests
import base64
import sqlite3
import datetime

def main():
    # Get all stats from today to 14 days back.
    end = datetime.datetime.today()
    start = end - datetime.timedelta(days=14)
    url = f"https://wakatime.com/api/v1/users/current/summaries?start={start.date()}&end={end.date()}"

    # Header has to contain you API key encoded in base64 and "Basic " prepended to it.
    key_base64 = base64.b64encode(key.encode("utf-8"))
    key_base64 = str(key_base64, "utf-8")
    header = {"content-type": "application/json", "Authorization": "Basic " + key_base64}

    # Make a request and save response as json object.
    response = requests.get(url, headers=header)
    response_json = response.json()

    with (sqlite3.connect(db_path) as cn:
        c = cn.cursor()

        c.execute("""CREATE TABLE IF NOT EXISTS wakatime_daily 
            (date TEXT PRIMARY KEY, total_seconds INTEGER)""")
        c.execute("""CREATE TABLE IF NOT EXISTS wakatime_projects 
            (date TEXT, name TEXT, total_seconds INTEGER, PRIMARY KEY(date, name))""")
        c.execute("""CREATE TABLE IF NOT EXISTS wakatime_machines 
            (date TEXT, name TEXT, total_seconds INTEGER, PRIMARY KEY(date, name))""")
        c.execute("""CREATE TABLE IF NOT EXISTS wakatime_languages 
            (date TEXT, name TEXT, total_seconds INTEGER, PRIMARY KEY(date, name))""")
        c.execute("""CREATE TABLE IF NOT EXISTS wakatime_editors 
            (date TEXT, name TEXT, total_seconds INTEGER, PRIMARY KEY(date, name))""")

        def _extract(dictionary, date) -> list:
            # Extracts name of project, machine, language, editor and seconds spent with/in/on it.
            return [(date, x["name"], int(x["total_seconds"])) for x in dictionary]

        for x in response_json["data"]:
            date = x["range"]["date"]
            total_time =  x["grand_total"]["total_seconds"]
            c.execute("INSERT OR REPLACE INTO wakatime_daily VALUES (?, ?)", (date, int(total_time)))

            projects = _extract(x["projects"], date)
            c.executemany("INSERT OR REPLACE INTO wakatime_projects VALUES (?, ?, ?)", projects)

            machines = _extract(x["machines"], date)
            c.executemany("INSERT OR REPLACE INTO wakatime_machines VALUES (?, ?, ?)", machines)

            languages = _extract(x["languages"], date)
            c.executemany("INSERT OR REPLACE INTO wakatime_languages VALUES (?, ?, ?)", languages)

            editors = _extract(x["editors"], date)
            c.executemany("INSERT OR REPLACE INTO wakatime_editors VALUES (?, ?, ?)", editors)


    print("wakatime data has been added to the database!")

if __name__ == "__main__":

    # Change this to whatever path you want to store it in:
    db_path = "wakatime.db"

    # Add your Wakatime API key here:
    key = ""


I’ve spent a few hours restructuring this mess. From now, most of the stuff lives in markdown files and those are then converted to proper HTML with a small Python script and a ./build.sh command. Could I save my time and life using Hugo? Absolutely. Will I do it? Nah.



This website is not dead yet.


01001001 00100000 01101011 01101110 01101111 01110111 00100000 01110100 01101000 01100001 01110100 00100000 01110100 01101000 01101001 01110011 00100000 01110000 01100001 01100111 01100101 00100000 01110111 01101001 01101100 01101100 00100000 01110011 01100101 01100101 00100000 01101101 01101111 01110010 01100101 00100000 01100010 01101111 01110100 00100000 01110100 01110010 01100001 01100110 01100110 01101001 01100011 00100000 01110100 01101000 01100001 01101110 00100000 01101000 01110101 01101101 01100001 01101110 00101100 00100000 01110011 01101111 00100000 01101000 01100101 01101100 01101100 01101111 00100000 01110100 01101000 01100101 01110010 01100101 00101100 00100000 01100010 01111001 01110100 01100101 00100000 01100110 01110010 01101001 01100101 01101110 01100100 01110011 00101110 00100000 01010011 01110000 01100001 01110010 01100101 00100000 01101101 01111001 00100000 01100110 01110101 01110100 01110101 01110010 01100101 00100000 01101010 01101111 01100010 00101100 00100000 01101111 01101011 01100001 01111001 00111111 00100000 01001001 00100000 01110111 01100001 01101110 01110100 00100000 01110100 01101111 00100000 01101000 01100001 01110110 01100101 00100000 01100001 00100000 01101010 01101111 01100010 00100000 01100110 01101111 01110010 00100000 01110011 01101111 01101101 01100101 00100000 01111001 01100101 01100001 01110010 01110011 00100000 01100010 01100101 01100110 01101111 01110010 01100101 00100000 01101000 01100001 01101100 01100110 00101101 01100001 01110011 01110011 01100101 01100100 00100000 01110011 01101001 01101100 01101001 01100011 01101111 01101110 00100000 01110110 01100001 01101100 01101100 01100101 01111001 00100000 01000001 01001001 00100000 01100001 01100011 01110100 01110101 01100001 01101100 01101100 01111001 00100000 01100011 01101111 01100100 01100101 01100100 00100000 01101001 01101110 00100000 01001001 01101110 01100100 01101001 01100001 00100000 01110111 01101001 01101100 01101100 00100000 01110010 01100101 01110000 01101100 01100001 01100011 01100101 00100000 01101101 01100101 00101110