Skip to content

Commit

Permalink
Initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
rebane2001 committed May 14, 2022
1 parent bcedb3b commit 26c7c27
Show file tree
Hide file tree
Showing 10 changed files with 337 additions and 0 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
# txnor-server
Server software running on txnor.com for the Discord s/e/x "hack".

`txnor.nginx` is an nginx site configuration.

Note: You'll have to supply your own impact.ttf
120 changes: 120 additions & 0 deletions chessgame.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
from PIL import Image, ImageFont, ImageDraw
from cairosvg import svg2png
import random
import re
import io

import chess
import chess.engine
import chess.svg

# Start the stockfish engine
engine = chess.engine.SimpleEngine.popen_uci("stockfish")

valid_move_pattern = re.compile("^[a-h][0-8][a-h][0-8]q?$")
valid_move_pattern_open = re.compile("[a-h][0-8][a-h][0-8]q?")

board_colors = {
"square light": "#FFFFFF",
"square dark": "#5865F2",
"square light lastmove": "#57F287",
"square dark lastmove": "#57F287",
}

"""
The implementation of python-chess and stockfish in this file was rushed and isn't very good.
Please refer to the actual documentation if you wish to use them in your own project.
"""


def process_url(url):
"""Generate moves from a URL."""
url = url.lower()
moves = valid_move_pattern_open.findall(url)[::-1]

# Generate a seed from the URL
seed = 0
seed_str = url.split("/")[-1]
if "vieag" in seed_str or "vixag" in seed_str:
seed_str = ""
for c in seed_str:
seed = (seed + ord(c)) % 1024

return generate_state(moves, seed)


def generate_state(moves, seed):
"""Generate a game state from the moves."""

# Create a new board and game
board = chess.Board()
game_id = random.random()
result = None

for move in moves:
try:
# Just skip invalid moves
if not valid_move_pattern.match(move):
continue
if chess.Move.from_uci(move) not in board.legal_moves:
continue

# Add player move
board.push(chess.Move.from_uci(move))
if board.is_game_over():
break

# Add stockfish move
# The way the engine is configured/seeded is bad, it is only left this way to keep older play URLs intact
result = engine.play(board, chess.engine.Limit(depth=1024 + seed, nodes=1024 + seed), game=game_id)
result = result.move
board.push(result)
if board.is_game_over():
break
except Exception as e:
# If a move errors, we just skip over the move
print(e)
continue

# Generate an SVG of the board
svg = chess.svg.board(board, lastmove=result, colors=board_colors, size=300)
board_png = io.BytesIO()
svg2png(bytestring=svg, write_to=board_png)

# Generate an empty image
base = Image.new("RGBA", (395, 300), (0, 0, 0, 0))
img = Image.open(board_png).convert("RGBA")

# Generate an empty image
base.paste(img, (0, 0), img)

# Draw moves list as text
draw = ImageDraw.Draw(base)
font = ImageFont.truetype("impact.ttf", 24)
draw.text((323, 3), "\n".join([str(stack).upper() for stack in board.move_stack[::-1]]), (255, 255, 255), font=font,
stroke_width=2, stroke_fill=(0, 0, 0))

# Draw example move text
font = ImageFont.truetype("impact.ttf", 14)
draw.text((304, 260), "example move:", (255, 255, 255), font=font, stroke_width=2, stroke_fill=(0, 0, 0))
font = ImageFont.truetype("impact.ttf", 18)
draw.text((304, 276), "s/g/$&b1c3", (255, 255, 255), font=font, stroke_width=2, stroke_fill=(0, 0, 0))

# If the game is over, draw the outcome
if board.is_game_over():
outcome = board.outcome()
font = ImageFont.truetype("impact.ttf", 96)
if outcome.winner is None:
draw.text((4, 84), "wtf draw?", (255, 255, 255), font=font, stroke_width=4, stroke_fill=(0, 0, 0))
elif outcome.winner == chess.WHITE:
draw.text((46, 84), "u win", (255, 255, 255), font=font, stroke_width=4, stroke_fill=(0, 0, 0))
elif outcome.winner == chess.BLACK:
draw.text((33, 84), "u lose", (255, 255, 255), font=font, stroke_width=4, stroke_fill=(0, 0, 0))

# Return the image to the client
out = io.BytesIO()
base.save(out, "PNG")
return out.getvalue()

# You can quit the engine like this:
# engine.quit()
Binary file added img/6969th.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/sex_base.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/sex_base_game.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/sex_base_old.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/sex_default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added requirements.txt
Binary file not shown.
167 changes: 167 additions & 0 deletions sex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
from PIL import Image, ImageFont, ImageDraw, ImageEnhance
import requests
import random
import web
import io
import re

# Chess
import chessgame

# Configure the app
app = web.application(('(.*)', 'SexHack'), globals())
web.config.debug = False
host = "127.0.0.1"
port = 8000

# Load images into memory
with open("img/sex_default.png", "rb") as f:
default_img = f.read()
with open("img/sex_base_game.png", "rb") as f:
base_img = f.read()
with open("img/6969th.png", "rb") as f:
six_nine = f.read()

# User-Agent for Tenor requests, it's nice to tell them who you are
headers = {
'User-Agent': 'A Discord Bot by an unknown person using github.com/rebane2001/txnor-server',
}


class SexHack:
@staticmethod
def default_response():
"""Return default image"""
return default_img

@staticmethod
def pony():
"""Return an image from Derpibooru"""
r = requests.get(
"https://derpibooru.org/api/v1/json/search/images?"
"q=pony,score.gte:1000,aspect_ratio:1,safe&sf=random&per_page=1",
headers=headers, timeout=3)
medium = r.json()["images"][0]["representations"]["medium"]
content = requests.get(medium, headers=headers, timeout=3).content
return content

@staticmethod
def math_challenge():
"""Return a random math challenge"""
# Create new image
img = Image.new("RGBA", (450, 135), (255, 255, 255, 255))

# Draw text on the image
draw = ImageDraw.Draw(img)
font = ImageFont.truetype("impact.ttf", 32)
draw.text((450 / 2, 10), f"Math challenge (99% fail):", (255, 255, 255), font=font, anchor="ma",
stroke_width=2, stroke_fill=(0, 0, 0))

# Generate a random math challenge
num_a = random.randint(2, 6)
num_b = int(random.randint(3, 12) * num_a)
num_c = random.randint(3, 24)

# Draw the math challenge as text
font = ImageFont.truetype("impact.ttf", 64)
draw.text((450 / 2, 50), f"{num_b} / {num_a} + {num_c}", (255, 255, 255), font=font, anchor="ma",
stroke_width=2, stroke_fill=(0, 0, 0))

# Return the image to the client
out = io.BytesIO()
img.save(out, "PNG")
return out.getvalue()

@staticmethod
def generate_img(name):
"""Generate s/e/x or double s/e/x image"""
url = f"https://tenor.com/view/{name[6:]}"
r = requests.get(url, headers=headers, timeout=3)
search = re.search(r'<meta class="dynamic" name="twitter:image" content="https://c.tenor.com/([^"]*)">',
r.text)
img_url = f"https://c.tenor.com/{search.group(1)}".replace("AAC/", "AAe/")
img_raw = requests.get(img_url, stream=True, headers=headers, timeout=3).raw

img = Image.open(img_raw).resize((350, 185)).convert("RGB")

draw = ImageDraw.Draw(img)
font = ImageFont.truetype("impact.ttf", 32)
draw.text((350 / 2, 0), "WTF DISCORD SEX", (255, 255, 255), font=font, anchor="ma", stroke_width=2,
stroke_fill=(0, 0, 0))
draw.text((350 / 2, 185), "??? WTF ??? SEX ???", (255, 255, 255), font=font, anchor="md", stroke_width=2,
stroke_fill=(0, 0, 0))

# JPEGify the image slightly
jpg = io.BytesIO()
img.save(jpg, "JPEG", quality=10)
img = Image.open(jpg).resize((190, 135)).convert("RGBA")

# Apply the GIF thumbnail to the template
base = Image.open(io.BytesIO(base_img))
base.paste(img, (5, 159))

double_sex = name[3] == "x"
if double_sex:
# Draw text
draw = ImageDraw.Draw(base)
font = ImageFont.truetype("impact.ttf", 36)
draw.text((394 / 2, -10), "ULTRA DOUBLE SEX", (255, 255, 255), font=font, anchor="ma", stroke_width=2,
stroke_fill=(0, 0, 0))
draw.text((394 / 2, 295), "ACTIVATED ??? HOW", (255, 255, 255), font=font, anchor="md", stroke_width=2,
stroke_fill=(0, 0, 0))

# Add a red-ish background
bg = Image.new("RGBA", base.size, (35, 0, 0, 255))
bg.paste(base, (0, 0), base)

# JPEGify and saturate the image
jpg = io.BytesIO()
bg.convert("RGB").resize((300, 200)).save(jpg, "JPEG", quality=50)
base = Image.open(jpg).convert("RGB")
base = ImageEnhance.Color(base).enhance(10.5).resize((395, 300))

# Return the image to the client
out = io.BytesIO()
base.save(out, "PNG")
return out.getvalue()

def handle_request(self, name):
try:
# 6969th winner image (disable for chess)
if random.randint(0, 6969) == 6969 and "ag" not in name:
web.header('Cache-Control', 'no-store')
return six_nine

# Math challenge
if re.search(r'^/(math|math_?challenge)/?[A-Za-z0-9_-]*$', name):
web.header('Cache-Control', 'no-store')
return self.math_challenge()

# /viqw/
if re.search(r'^/vi(q|questrian?)w/[A-Za-z0-9_-]*$', name):
web.header('Cache-Control', 'no-store')
return self.pony()

# Enable caching from this point onward
web.header('Cache-Control', 'public, max-age=86400')

# /vieag/ and /vixag/
if re.search(r'^/vi[ex]ag[A-Za-z0-9]*/[A-Za-z0-9_-]*$', name):
return chessgame.process_url(name)

# /view/ and /vixw/
if re.search(r'^/vi[ex]w/[A-Za-z0-9_-]*$', name):
return self.generate_img(name)
except Exception as e:
"""We catch Exceptions because we want to show the default image instead of an error page"""
print(e)

def GET(self, name):
"""Handle a request"""
web.header('Content-type', 'image/png')
# Return default image if one was not generated
return self.handle_request(name) or self.default_response()


if __name__ == "__main__":
web.httpserver.runsimple(app.wsgifunc(), (host, port))
46 changes: 46 additions & 0 deletions txnor.nginx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=STATIC:16m inactive=24h max_size=8g;

server {
server_name txnor.com c.txnor.com;

charset UTF-8;
root /var/www/txnor.com;

location @fallback {
if ($http_user_agent ~* "(Intel Mac OS X 11.6; rv:92.0|Discord)") {
rewrite ^ /discord_sex.png break;
}
}

location / {
# Cache
proxy_buffering on;
proxy_cache STATIC;
proxy_cache_valid 200 1d;
proxy_cache_use_stale error timeout invalid_header updating
http_500 http_502 http_503 http_504;

# Request from Discord
if ($http_user_agent ~* "(Intel Mac OS X 11.6; rv:92.0|Discord)") {
error_page 502 = @fallback;
proxy_pass http://127.0.0.1:8000;
}

# Request from anyone else
if ($http_user_agent !~* "(Intel Mac OS X 11.6; rv:92.0|Discord)") {
return 301 https://twitter.com/rebane2001/status/1521538992524644352;
}
}
listen 443 ssl;

# Certificate stuff here
}

server {
if ($host = txnor.com) {
return 301 https://$host$request_uri;
}

listen 80 ;
server_name txnor.com;
}

0 comments on commit 26c7c27

Please sign in to comment.