remotes/1693045480750635534/spooky-22
Aevann1 2021-10-27 22:12:16 +02:00
parent df451a9062
commit 527756e90a
10 changed files with 198 additions and 150 deletions

View File

@ -2,66 +2,66 @@ version: '2.3'
services:
files:
build:
context: .
volumes:
- "./:/service"
environment:
- DATABASE_URL=postgresql://postgres@postgres:5432
- MASTER_KEY=${MASTER_KEY:-KTVciAUQFpFh2WdJ/oiHJlxl6FvzRZp8kYzAAv3l2OA=}
- REDIS_URL=redis://redis
- DOMAIN=localhost
- SITE_NAME=Drama
- GIPHY_KEY=3435tdfsdudebussylmaoxxt43
- FORCE_HTTPS=0
- DISCORD_SERVER_ID=3435tdfsdudebussylmaoxxt43
- DISCORD_CLIENT_ID=3435tdfsdudebussylmaoxxt43
- DISCORD_CLIENT_SECRET=3435tdfsdudebussylmaoxxt43
- DISCORD_BOT_TOKEN=3435tdfsdudebussylmaoxxt43
#- HCAPTCHA_SITEKEY=3435tdfsdudebussylmaoxxt43
- HCAPTCHA_SECRET=3435tdfsdudebussylmaoxxt43
- YOUTUBE_KEY=3435tdfsdudebussylmaoxxt43
- PUSHER_KEY=3435tdfsdudebussylmaoxxt43
- CATBOX_KEY=3435tdfsdudebussylmaoxxt43
- SPAM_SIMILARITY_THRESHOLD=0.5
- SPAM_SIMILAR_COUNT_THRESHOLD=5
- SPAM_URL_SIMILARITY_THRESHOLD=0.1
- COMMENT_SPAM_SIMILAR_THRESHOLD=0.5
- COMMENT_SPAM_COUNT_THRESHOLD=5
- READ_ONLY=0
- BOT_DISABLE=0
- COINS_NAME=Dramacoins
- DEFAULT_TIME_FILTER=all
- DEFAULT_THEME=midnight
- DEFAULT_COLOR=ff66ac #YOU HAVE TO PICK ONE OF THOSE COLORS OR SHIT WILL BREAK: ff66ac, 805ad5, 62ca56, 38a169, 80ffff, 2a96f3, eb4963, ff0000, f39731, 30409f, 3e98a7, e4432d, 7b9ae4, ec72de, 7f8fa6, f8db58
- SLOGAN=Dude bussy lmao
- GUMROAD_TOKEN=3435tdfsdudebussylmaoxxt43
- GUMROAD_LINK=https://marsey1.gumroad.com/l/tfcvri
- CARD_VIEW=1
- DISABLE_DOWNVOTES=0
- DUES=0
- MAIL_USERNAME=blahblahblah@gmail.com
- MAIL_PASSWORD=3435tdfsdudebussylmaoxxt43
links:
- "redis"
- "postgres"
ports:
- "80:80"
depends_on:
- redis
- postgres
build:
context: .
volumes:
- "./:/service"
environment:
- DATABASE_URL=postgresql://postgres@postgres:5432
- MASTER_KEY=${MASTER_KEY:-KTVciAUQFpFh2WdJ/oiHJlxl6FvzRZp8kYzAAv3l2OA=}
- REDIS_URL=redis://redis
- DOMAIN=0.0.0.0
- SITE_NAME=Drama
- GIPHY_KEY=3435tdfsdudebussylmaoxxt43
- FORCE_HTTPS=0
- DISCORD_SERVER_ID=3435tdfsdudebussylmaoxxt43
- DISCORD_CLIENT_ID=3435tdfsdudebussylmaoxxt43
- DISCORD_CLIENT_SECRET=3435tdfsdudebussylmaoxxt43
- DISCORD_BOT_TOKEN=3435tdfsdudebussylmaoxxt43
#- HCAPTCHA_SITEKEY=3435tdfsdudebussylmaoxxt43
- HCAPTCHA_SECRET=3435tdfsdudebussylmaoxxt43
- YOUTUBE_KEY=3435tdfsdudebussylmaoxxt43
- PUSHER_KEY=3435tdfsdudebussylmaoxxt43
- CATBOX_KEY=3435tdfsdudebussylmaoxxt43
- SPAM_SIMILARITY_THRESHOLD=0.5
- SPAM_SIMILAR_COUNT_THRESHOLD=5
- SPAM_URL_SIMILARITY_THRESHOLD=0.1
- COMMENT_SPAM_SIMILAR_THRESHOLD=0.5
- COMMENT_SPAM_COUNT_THRESHOLD=5
- READ_ONLY=0
- BOT_DISABLE=0
- COINS_NAME=Dramacoins
- DEFAULT_TIME_FILTER=all
- DEFAULT_THEME=midnight
- DEFAULT_COLOR=ff66ac #YOU HAVE TO PICK ONE OF THOSE COLORS OR SHIT WILL BREAK: ff66ac, 805ad5, 62ca56, 38a169, 80ffff, 2a96f3, eb4963, ff0000, f39731, 30409f, 3e98a7, e4432d, 7b9ae4, ec72de, 7f8fa6, f8db58
- SLOGAN=Dude bussy lmao
- GUMROAD_TOKEN=3435tdfsdudebussylmaoxxt43
- GUMROAD_LINK=https://marsey1.gumroad.com/l/tfcvri
- CARD_VIEW=1
- DISABLE_DOWNVOTES=0
- DUES=0
- MAIL_USERNAME=blahblahblah@gmail.com
- MAIL_PASSWORD=3435tdfsdudebussylmaoxxt43
links:
- "redis"
- "postgres"
ports:
- "80:80"
depends_on:
- redis
- postgres
redis:
image: redis
ports:
- "6379:6379"
image: redis
ports:
- "6379:6379"
postgres:
image: postgres:12.3
volumes:
- "./schema.sql:/docker-entrypoint-initdb.d/00-schema.sql"
- "./seed-db.sql:/docker-entrypoint-initdb.d/01-schema.sql"
environment:
- POSTGRES_HOST_AUTH_METHOD=trust
ports:
- "5432:5432"
image: postgres:12.3
volumes:
- "./schema.sql:/docker-entrypoint-initdb.d/00-schema.sql"
- "./seed-db.sql:/docker-entrypoint-initdb.d/01-schema.sql"
environment:
- POSTGRES_HOST_AUTH_METHOD=trust
ports:
- "5432:5432"

View File

@ -48,7 +48,7 @@ class User(Base):
patron = Column(Integer, default=0)
verified = Column(String)
verifiedcolor = Column(String)
marseyawarded = Column(String)
marseyawarded = Column(Integer)
email = Column(String)
css = deferred(Column(String))
profilecss = deferred(Column(String))

View File

@ -530,7 +530,7 @@ def award_post(pid, v):
author.ban(reason=f"1-Day ban award used by @{v.username} on /post/{post.id}", days=1)
send_notification(author.id, f"Your account has been suspended for a day for {link}. It sucked and you should feel bad.")
elif author.unban_utc > 0:
author.unban_utc += 24*60*60
author.unban_utc += 86400
send_notification(author.id, f"Your account has been suspended for yet another day for {link}. Seriously man?")
elif kind == "unban":
if not author.is_suspended or not author.unban_utc or time.time() > author.unban_utc: abort(403)
@ -587,7 +587,7 @@ def award_post(pid, v):
new_badge = Badge(badge_id=67, user_id=author.id)
g.db.add(new_badge)
elif kind == "marsey":
author.marseyawarded = True
author.marseyawarded = time.time() + 86400
post.author.received_award_count += 1
g.db.add(post.author)
@ -652,7 +652,7 @@ def award_comment(cid, v):
author.ban(reason=f"1-Day ban award used by @{v.username} on /comment/{c.id}", days=1)
send_notification(author.id, f"Your account has been suspended for a day for {link}. It sucked and you should feel bad.")
elif author.unban_utc > 0:
author.unban_utc += 24*60*60
author.unban_utc += 86400
send_notification(author.id, f"Your account has been suspended for yet another day for {link}. Seriously man?")
elif kind == "unban":
if not author.is_suspended or not author.unban_utc or time.time() > author.unban_utc: abort(403)
@ -706,7 +706,7 @@ def award_comment(cid, v):
new_badge = Badge(badge_id=67, user_id=author.id)
g.db.add(new_badge)
elif kind == "marsey":
author.marseyawarded = True
author.marseyawarded = time.time() + 86400
c.author.received_award_count += 1
g.db.add(c.author)
@ -728,8 +728,7 @@ def admin_userawards_get(v):
@validate_formkey
def admin_userawards_post(v):
if v.admin_level < 6:
abort(403)
if v.admin_level < 6: abort(403)
try: u = request.values.get("username").strip()
except: abort(404)
@ -746,8 +745,7 @@ def admin_userawards_post(v):
if value:
if int(value) > 0:
notify_awards[key] = int(value)
if int(value) > 0: notify_awards[key] = int(value)
for x in range(int(value)):
thing += 1
@ -762,14 +760,13 @@ def admin_userawards_post(v):
text = "You were given the following awards:\n\n"
for key, value in notify_awards.items():
text += f" - **{value}** {AWARDS[key]['title']} {'Awards' if value != 1 else 'Award'}\n"
for key, value in notify_awards.items(): text += f" - **{value}** {AWARDS[key]['title']} {'Awards' if value != 1 else 'Award'}\n"
send_notification(u.id, text)
g.db.commit()
if v.username == "Aevann": return render_template("admin/awards.html", awards=list(AWARDS.values()), v=v)
if request.host == 'rdrama.net' and v.id in [1,28,995]: return render_template("admin/awards.html", awards=list(AWARDS.values()), v=v)
return render_template("admin/awards.html", awards=list(AWARDS2.values()), v=v)

View File

@ -156,8 +156,12 @@ def api_comment(v):
body = body.strip()
if v.marseyawarded:
marregex = list(re.finditer("^(:!?m\w+:\s*)+$", body))
if len(marregex) == 0: return {"error":"You need to only type marseys!"}, 403
if time.time() > v.marseyawarded:
v.marseyawarded = None
g.db.add(v)
else:
marregex = list(re.finditer("^(:!?m\w+:\s*)+$", body))
if len(marregex) == 0: return {"error":"You can only type marseys!"}, 403
if not body and not request.files.get('file'): return {"error":"You need to actually write something!"}, 400
@ -187,6 +191,8 @@ def api_comment(v):
body_md = CustomRenderer().render(mistletoe.Document(body_md))
body_html = sanitize(body_md)
if v.marseyawarded and len(list(re.finditer('>[^<]|[^>]<', body_html))) > 0: return {"error":"You can only type marseys!"}, 403
bans = filter_comment_html(body_html)
if bans:
@ -590,14 +596,20 @@ def edit_comment(cid, v):
body = request.values.get("body", "").strip()[:10000]
if v.marseyawarded:
marregex = list(re.finditer("^(:!?m\w+:\s*)+$", body))
if len(marregex) == 0: return {"error":"You need to only type marseys!"}, 403
if time.time() > v.marseyawarded:
v.marseyawarded = None
g.db.add(v)
else:
marregex = list(re.finditer("^(:!?m\w+:\s*)+$", body))
if len(marregex) == 0: return {"error":"You can only type marseys!"}, 403
for i in re.finditer('^(https:\/\/.*\.(png|jpg|jpeg|gif|webp|PNG|JPG|JPEG|GIF|WEBP|9999))', body, re.MULTILINE):
if "wikipedia" not in i.group(1): body = body.replace(i.group(1), f'![]({i.group(1)})')
body_md = CustomRenderer().render(mistletoe.Document(body))
body_html = sanitize(body_md)
if v.marseyawarded and len(list(re.finditer('>[^<]|[^>]<', body_html))) > 0: return {"error":"You can only type marseys!"}, 403
bans = filter_comment_html(body_html)
if bans:

View File

@ -215,15 +215,21 @@ def edit_post(pid, v):
body = request.values.get("body", "").strip()
if v.marseyawarded:
marregex = list(re.finditer("^(:!?m\w+:\s*)+$", title))
if len(marregex) == 0: return {"error":"You need to only type marseys!"}, 403
if body:
marregex = list(re.finditer("^(:!?m\w+:\s*)+$", body))
if len(marregex) == 0: return {"error":"You need to only type marseys!"}, 403
if time.time() > v.marseyawarded:
v.marseyawarded = None
g.db.add(v)
else:
marregex = list(re.finditer("^(:!?m\w+:\s*)+$", title))
if len(marregex) == 0: return {"error":"You can only type marseys!"}, 403
if body:
marregex = list(re.finditer("^(:!?m\w+:\s*)+$", body))
if len(marregex) == 0: return {"error":"You can only type marseys!"}, 403
if title != p.title:
title_html = filter_title(title)
if v.marseyawarded and len(list(re.finditer('>[^<]|[^>]<', title_html))) > 0: return {"error":"You can only type marseys!"}, 403
p.title = title
p.title_html = filter_title(title)
p.title_html = title_html
if body != p.body:
for i in re.finditer('^(https:\/\/.*\.(png|jpg|jpeg|gif|webp|PNG|JPG|JPEG|GIF|WEBP|9999))', body, re.MULTILINE):
@ -241,6 +247,7 @@ def edit_post(pid, v):
return {"error": reason}, 403
p.body = body
if v.marseyawarded and len(list(re.finditer('>[^<]|[^>]<', body_html))) > 0: return {"error":"You can only type marseys!"}, 40
p.body_html = body_html
if "rama" in request.host and "ivermectin" in body_html.lower():
@ -405,7 +412,7 @@ def thumbnail_thread(pid):
thumb_candidate_urls=[]
meta_tags = [
"ruqqus:thumbnail",
"drama:thumbnail",
"twitter:image",
"og:image",
"thumbnail"
@ -495,6 +502,8 @@ def submit_post(v):
title = request.values.get("title", "").strip()
url = request.values.get("url", "").strip()
title_html = filter_title(title)
if v.marseyawarded and len(list(re.finditer('>[^<]|[^>]<', title_html))) > 0: return {"error":"You can only type marseys!"}, 40
if url:
if "/i.imgur.com/" in url: url = url.replace(".png", ".webp").replace(".jpg", ".webp").replace(".jpeg", ".webp")
@ -513,16 +522,16 @@ def submit_post(v):
domain = parsed_url.netloc
# qd = parse_qs(parsed_url.query)
# filtered = dict((k, v) for k, v in qd.items() if not k.startswith('utm_'))
qd = parse_qs(parsed_url.query)
filtered = dict((k, v) for k, v in qd.items() if not k.startswith('utm_'))
# new_url = ParseResult(scheme="https",
# netloc=parsed_url.netloc,
# path=parsed_url.path,
# params=parsed_url.params,
# query=urlencode(filtered, doseq=True),
# fragment=parsed_url.fragment)
# url = urlunparse(new_url)
new_url = ParseResult(scheme="https",
netloc=parsed_url.netloc,
path=parsed_url.path,
params=parsed_url.params,
query=urlencode(filtered, doseq=True),
fragment=parsed_url.fragment)
url = urlunparse(new_url)
repost = g.db.query(Submission).options(lazyload('*')).filter(
Submission.url.ilike(url),
@ -570,11 +579,15 @@ def submit_post(v):
body = request.values.get("body", "").strip()
if v.marseyawarded:
marregex = list(re.finditer("^(:!?m\w+:\s*)+$", title))
if len(marregex) == 0: return {"error":"You need to only type marseys!"}, 403
if body:
marregex = list(re.finditer("^(:!?m\w+:\s*)+$", body))
if len(marregex) == 0: return {"error":"You need to only type marseys!"}, 403
if time.time() > v.marseyawarded:
v.marseyawarded = None
g.db.add(v)
else:
marregex = list(re.finditer("^(:!?m\w+:\s*)+$", title))
if len(marregex) == 0: return {"error":"You can only type marseys!"}, 403
if body:
marregex = list(re.finditer("^(:!?m\w+:\s*)+$", body))
if len(marregex) == 0: return {"error":"You can only type marseys!"}, 403
dup = g.db.query(Submission).options(lazyload('*')).filter(
Submission.author_id == v.id,
@ -606,14 +619,11 @@ def submit_post(v):
Submission.url.op('<->')(url) < app.config["SPAM_URL_SIMILARITY_THRESHOLD"],
Submission.created_utc > cutoff
).all()
else:
similar_urls = []
else: similar_urls = []
threshold = app.config["SPAM_SIMILAR_COUNT_THRESHOLD"]
if v.age >= (60 * 60 * 24 * 7):
threshold *= 3
elif v.age >= (60 * 60 * 24):
threshold *= 2
if v.age >= (60 * 60 * 24 * 7): threshold *= 3
elif v.age >= (60 * 60 * 24): threshold *= 2
if max(len(similar_urls), len(similar_posts)) >= threshold:
@ -663,7 +673,7 @@ def submit_post(v):
body_md = CustomRenderer().render(mistletoe.Document(body))
body_html = sanitize(body_md)
if v.marseyawarded and len(list(re.finditer('>[^<]|[^>]<', body_html))) > 0: return {"error":"You can only type marseys!"}, 40
if len(body_html) > 20000: abort(400)
@ -690,7 +700,7 @@ def submit_post(v):
body_html=body_html,
embed_url=embed,
title=title,
title_html=filter_title(title)
title_html=title_html
)
g.db.add(new_post)
@ -859,33 +869,28 @@ def submit_post(v):
else: body = random.choice(snappyquotes)
body += "\n\n---\n\n"
else: body = ""
if new_post.url:
body += f"Snapshots:\n\n* [reveddit.com](https://reveddit.com/{new_post.url})\n* [archive.org](https://web.archive.org/{new_post.url})\n* [archive.ph](https://archive.ph/?url={quote(new_post.url)}&run=1) (click to archive)\n\n"
gevent.spawn(archiveorg, new_post.url)
# archive other urls in post
url_regex = '<a (target=\"_blank\" )?(rel=\"nofollow noopener noreferrer\" )?href=\"(https?://[a-z]{1,20}\.[^\"]+)\"( rel=\"nofollow noopener noreferrer\" target=\"_blank\")?>([^\"]+)</a>'
_body = new_post.body_html
#print(_body)
for url_match in re.finditer(url_regex, _body, flags=re.M|re.I):
href = url_match.group(3)
url_regex = '<a (target=\"_blank\" )?(rel=\"nofollow noopener noreferrer\" )?href=\"(https?://[a-z]{1,20}\.[^\"]+)\"( rel=\"nofollow noopener noreferrer\" target=\"_blank\")?>([^\"]+)</a>'
for url_match in re.finditer(url_regex, new_post.body_html, flags=re.M|re.I):
href = url_match.group(3)
if not href: continue
if not href:
#print(f'{url_match.group(0)} skip')
continue
title = url_match.group(5)
if "Snapshots:\n\n" not in body: body += "Snapshots:\n\n"
#print(href)
title = url_match.group(5)
body += f'**[{title}]({href})**:\n\n'
body += f'* [reveddit.com](https://reveddit.com/{href})\n'
body += f'* [archive.org](https://web.archive.org/{href})\n'
body += f'* [archive.ph](https://archive.ph/?url={quote(href)}&run=1) (click to archive)\n\n'
gevent.spawn(archiveorg, href)
body += f'**[{title}]({href})**:\n\n'
body += f'* [reveddit.com](https://reveddit.com/{href})\n'
body += f'* [archive.org](https://web.archive.org/{href})\n'
body += f'* [archive.ph](https://archive.ph/?url={quote(href)}&run=1) (click to archive)\n\n'
gevent.spawn(archiveorg, href)
body_md = CustomRenderer().render(mistletoe.Document(body))
body_html = sanitize(body_md)
c = Comment(author_id=SNAPPY_ACCOUNT,
distinguish_level=6,
parent_submission=new_post.id,

View File

@ -500,6 +500,8 @@ def settings_images_profile(v):
if not imageurl: abort(400)
if v.highres and '/images/' in v.highres : os.remove('/images/' + v.highres.split('/images/')[1])
if v.profileurl and '/images/' in v.profileurl : os.remove('/images/' + v.profileurl.split('/images/')[1])
v.highres = highres
v.profileurl = imageurl
g.db.add(v)
@ -522,10 +524,11 @@ def settings_images_banner(v):
name = f'/images/{int(time.time())}{secrets.token_urlsafe(2)}.gif'
file.save(name)
imageurl = request.host_url[:-1] + process_image(name)
bannerurl = request.host_url[:-1] + process_image(name)
if imageurl:
v.bannerurl = imageurl
if bannerurl:
if v.bannerurl and '/images/' in v.bannerurl : os.remove('/images/' + v.bannerurl.split('/images/')[1])
v.bannerurl = bannerurl
g.db.add(v)
g.db.commit()

View File

@ -160,10 +160,11 @@ def leaderboard(v):
users3 = users.order_by(User.post_count.desc()).limit(10).all()
users4 = users.order_by(User.comment_count.desc()).limit(10).all()
users5 = users.order_by(User.received_award_count.desc()).limit(10).all()
users7 = users.order_by(User.coins_spent.desc()).limit(10).all()
if 'pcmemes.net' in request.host:
users6 = users.order_by(User.basedcount.desc()).limit(10).all()
return render_template("leaderboard.html", v=v, users1=users1, users2=users2, users3=users3, users4=users4, users5=users5, users6=users6)
return render_template("leaderboard.html", v=v, users1=users1, users2=users2, users3=users3, users4=users4, users5=users5)
return render_template("leaderboard.html", v=v, users1=users1, users2=users2, users3=users3, users4=users4, users5=users5, users6=users6, users7=users7)
return render_template("leaderboard.html", v=v, users1=users1, users2=users2, users3=users3, users4=users4, users5=users5, users7=users7)
@app.get("/@<username>/css")

View File

@ -36,31 +36,29 @@
<h5>User Award Grant</h5>
<form action="/admin/awards", method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<label for="input-username">Username</label><br>
<input id="input-username" class="form-control mb-3" type="text" name="username" required>
<table class="table table-striped">
<thead class="bg-primary text-white">
<tr>
<th scope="col">Icon</th>
<th scope="col">Title</th>
<th scope="col">Amount</th>
</tr>
</thead>
<tbody>
{% for a in awards %}
<tr>
<td><i class="{{a['icon']}} {{a['color']}}" style="font-size: 30px"></i></td>
<td style="font-weight: bold">{{a['title']}}</td>
<td><input type="number" class="form-control" name="{{a['kind']}}" value="0" max="10" placeholder="Enter amount..." ></td>
</tr>
{% endfor %}
</table>
<label for="input-username">Username</label><br>
<input id="input-username" class="form-control mb-3" type="text" name="username" required>
<table class="table table-striped">
<thead class="bg-primary text-white">
<tr>
<th scope="col">Icon</th>
<th scope="col">Title</th>
<th scope="col">Amount</th>
</tr>
</thead>
<tbody>
{% for a in awards %}
<tr>
<td><i class="{{a['icon']}} {{a['color']}}" style="font-size: 30px"></i></td>
<td style="font-weight: bold">{{a['title']}}</td>
<td><input type="number" class="form-control" name="{{a['kind']}}" value="0" placeholder="Enter amount..." ></td>
</tr>
{% endfor %}
</table>
<input class="btn btn-primary mt-3" type="submit" value="Grant Awards">
<input class="btn btn-primary mt-3" type="submit" value="Grant Awards">
</form>

View File

@ -10,7 +10,7 @@
<div class="modal desktop-expanded-image-modal" id="expandImageModal" tabindex="-1" role="dialog" aria-labelledby="expandImageModalTitle" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered mx-auto expandedimage" role="document">
<div class="modal-content bg-transparent shadow-none m-4 m-md-0">
<div class="modal-content bg-transparent shadow-none m-5 m-md-0">
<div class="modal-body text-center p-0">
<div class="d-inline-block position-relative">
@ -31,4 +31,8 @@
width: fit-content;
width: -moz-fit-content;
}
.m5 {
margin: 2.5rem !important;
}
</style>

View File

@ -124,6 +124,34 @@
</tr>
{% endfor %}
</table>
<pre>
</pre>
<h5 style="font-weight:bold;text-align: center;">Top 10 by coins spent in shop</h5>
<pre>
</pre>
<table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th style="font-weight:bold;">#</th>
<th style="font-weight:bold;">Name</th>
<th style="font-weight:bold; text-align:right;">Coins</th>
</tr>
</thead>
{% for user in users7 %}
<tr>
<td style="font-weight:bold;">{{users7.index(user)+1}}</td>
<td><a style="color:#{{user.namecolor}}; font-weight:bold; fonts" href="/@{{user.username}}"><img loading="lazy" src="/uid/{{user.id}}/pic/profile" class="profile-pic-20 mr-1"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}};"{% endif %}>{{user.username}}</span></a></td>
<td style="font-weight:bold; text-align:right;">{{user.coins_spent}}</td>
</tr>
{% endfor %}
</table>
{% if "pcm" in request.host %}
<pre>