import base64
import logging
import os
-import random
-import time
from datetime import datetime, timezone
from logging.handlers import RotatingFileHandler
from pathlib import Path
@app.route("/")
def index():
- start = time.perf_counter()
all_users = db.query("select * from users order by username asc")
all_users = [dict(row) for row in all_users]
for user in all_users:
user["has_pfp"] = users.user_has_pfp(user["userid"])
for key, value in users.get_user_colors(user).items():
user[key] = value
- app.logger.info(f"Homepage users in {time.perf_counter() - start} seconds")
-
- titles = [
- ("Little Song Place", 2.0),
- ("Lumpy Space Princess", 0.2),
- ("Language Server Protocol", 0.1),
- ("Liskov Substitution Principle", 0.1),
- ]
- titles, weights = zip(*titles)
- title = random.choices(titles, weights)[0]
-
- start = time.perf_counter()
rows = db.query(
"""
SELECT * FROM jams
for j in all_jams:
all_events.extend(j.events)
ongoing_events, upcoming_events, _, _ = jams._sort_events(all_events)
- app.logger.info(f"Homepage jams in {time.perf_counter() - start} seconds")
random_songs = songs.get_random(3)
# Group songs by userid
- start = time.perf_counter()
page_songs = songs.get_latest(100)
songs_by_user = []
prev_song_user = None
songs_by_user.append([])
prev_song_user = song.userid
songs_by_user[-1].append(song)
- app.logger.info(f"Homepage songs in {time.perf_counter() - start} seconds")
- start = time.perf_counter()
- page = render_template(
+ return render_template(
"index.html",
users=all_users,
random_songs=random_songs,
songs_by_user=songs_by_user,
- page_title=title,
ongoing_events=ongoing_events,
upcoming_events=upcoming_events)
- app.logger.info(f"Homepage render in {time.perf_counter() - start} seconds")
-
- return page
@app.get("/site-news")
def site_news():
from flask import Blueprint, redirect, render_template, session
-from . import comments, db, songs
+from . import comments, db, push_notifications, songs
bp = Blueprint("activity", __name__)
[timestamp, session["userid"]])
db.commit()
- return render_template("activity.html", comments=notifications)
+ comment_push = False
+ song_push = False
+ if "subid" in session:
+ row = db.query(
+ "SELECT settings FROM users_push_subscriptions WHERE subid = ?",
+ [session["subid"]], one=True)
+ if row:
+ comment_push = (row["settings"] & push_notifications.SubscriptionSetting.COMMENTS) > 0
+ song_push = (row["settings"] & push_notifications.SubscriptionSetting.SONGS) > 0
+
+ return render_template("activity.html", comments=notifications, comment_push=comment_push, song_push=song_push)
@bp.get("/new-activity")
def new_activity():
import json
import threading
+import enum
import pywebpush
-from flask import Blueprint, current_app, g, request
+from flask import Blueprint, current_app, g, request, session
from . import auth, datadir, db
bp = Blueprint("push-notifications", __name__, url_prefix="/push-notifications")
+class SubscriptionSetting(enum.IntEnum):
+ COMMENTS = 0x0001
+ SONGS = 0x0002
+
@bp.post("/subscribe")
@auth.requires_login
def subscribe():
# Request must contain valid subscription JSON
abort(400)
- db.query(
+ row = db.query(
"""
- INSERT INTO users_push_subscriptions (userid, subscription)
- VALUES (?, ?)
+ INSERT INTO users_push_subscriptions (userid, subscription, settings)
+ VALUES (?, ?, ?)
+ RETURNING subid
""",
- [g.userid, json.dumps(request.json)])
+ [g.userid, json.dumps(request.json), 0], expect_one=True)
db.commit()
current_app.logger.info(f"{g.username} registered push subscription")
+ session["subid"] = row["subid"]
+
+ return {"status": "success"}
+
+@bp.post("/update-settings")
+@auth.requires_login
+def update_settings():
+ if not request.json:
+ # Request must contain valid subscription JSON
+ abort(400)
+
+ if "subid" not in session:
+ return {"status": "failed", "message": "no subid in current session"}
+
+ bitfield = 0
+ settings = request.json
+ if settings["comments"]:
+ bitfield |= SubscriptionSetting.COMMENTS
+ if settings["songs"]:
+ bitfield |= SubscriptionSetting.SONGS
+
+ db.query(
+ """
+ UPDATE users_push_subscriptions
+ SET settings = ?
+ WHERE subid = ? AND userid = ?
+ """,
+ [bitfield, session["subid"], g.userid])
+ db.commit()
+
+ current_app.logger.info(f"{g.username} updated push subscription settings: {bitfield:04x}")
+
return {"status": "success"}
def get_user_subscriptions(userid):
subid INTEGER PRIMARY KEY,
userid INTEGER NOT NULL,
subscription TEXT NOT NULL,
+ settings INTEGER NOT NULL,
FOREIGN KEY(userid) REFERENCES users(userid) ON DELETE CASCADE
);
let date = new Date(e.dataset.date);
e.textContent = date.toLocaleString();
});
-});
-async function requestNotificationPermission() {
- const permission = await window.Notification.requestPermission();
- if (permission === "granted") {
- // Register service worker
+ // Register service worker
+ if ("serviceWorker" in navigator)
+ {
navigator.serviceWorker.register("service.js");
}
- else {
- console.log("Did not get permission to send notifications:", permission);
- }
-}
+});
function onLinkClick(event) {
if (event.defaultPrevented) {
-const vapid_public_key = "BLsO37LostwqKch7SFr5Df0MexEoBOcujdMRY7wJurRPc_MGdz9rAkMrqs_dil4qSFxVbVyAA3FqLEPSL-WRNZs";
-
-self.addEventListener("activate", async () => {
- try {
- // Subscribe via browser's push service
- const options = {userVisibleOnly: true, applicationServerKey: vapid_public_key};
- const subscription = await self.registration.pushManager.subscribe(options);
- console.log(JSON.stringify(subscription));
-
- // Register subscription with LSP server
- const response = await fetch(
- "/push-notifications/subscribe", {
- method: "post",
- headers: {"Content-Type": "application/json"},
- body: JSON.stringify(subscription)
- }
- );
- console.log(response);
- }
- catch (err) {
- console.log("Error while activating service:", err);
- }
-});
self.addEventListener("push", (event) => {
if (event.data) {
box-shadow: -2px -2px 0px 0px var(--black);
}
+.boxy {
+ margin: 10px 0px;
+ padding: 10px;
+ box-shadow: 2px 2px 0px 0px;
+ border: 1px solid;
+ border-radius: var(--radius);
+}
+
+.boxy-lite {
+ margin: 10px 0px;
+ padding: 10px;
+ box-shadow: 1px 1px 0px 0px;
+ border: 1px solid;
+ border-radius: var(--radius);
+}
+
select {
border: none;
background-color: var(--purple);
margin: 10px;
}
-div.top-level-comment {
- margin-top: 10px;
- padding: 10px;
- border: 2px solid;
- border-radius: var(--radius);
-}
-
-div.reply-comment {
- margin-top: 10px;
- margin-bottom: 10px;
- padding: 10px;
- border: 2px solid;
- border-radius: var(--radius);
-}
-
div.comment-button-container {
display: flex;
gap: 10px;
font-size: 14px;
}
-/* Activity */
-div.comment-notification {
- margin: 10px;
- padding: 10px;
- box-shadow: 2px 2px 0px 0px;
- border: 1px solid;
- border-radius: var(--radius);
-}
-
/* Platform-specific global overrides */
@media screen and (max-width: 480px) {
.desktop-only {
{% block title %}Activity{% endblock %}
{% block body %}
-
-{% if comments %}
<h1>activity</h1>
- {% for comment in comments %}
- <div class="comment-notification">
- <a href="/users/{{ comment['comment_username'] }}" class="profile-link">{{ comment['comment_username'] }}</a>
- {% if comment['replyto_content'] %}
- replied to "{{ comment['replyto_content'] }}"
- {% else %}
- commented
- {% endif %}
- on
- {% if 'songid' in comment %}
- <a href="/song/{{ comment['content_userid'] }}/{{ comment['songid'] }}?action=view">{{ comment['title'] }}</a> -
- {# Nothing to do for user profile #}
- {% elif 'playlistid' in comment %}
- <a href="/playlists/{{ comment['playlistid'] }}">{{ comment['name'] }}</a> -
- {% elif 'eventid' in comment %}
- <a href="/jams/{{ comment['jamid']}}/events/{{ comment['eventid'] }}">{{ comment['title'] }}</a> -
- {% endif %}
- <a href="/users/{{ comment['content_username'] }}" class="profile-link">{{ comment['content_username'] }}</a>
- <div class="top-level-comment">
- <a href="/users/{{ comment['comment_username'] }}" class="profile-link">{{ comment['comment_username'] }}</a>:
- {{ comment['content'] }}
-
- <div class="comment-button-container">
- {% if comment['replytoid'] %}
- <!-- Comment is already part of a thread; reply to the same thread -->
- <a href="/comment?threadid={{ comment['threadid'] }}&replytoid={{ comment['replytoid'] }}">Reply</a>
- {% else %}
- <!-- Comment is a top-level, reply to the comment -->
- <a href="/comment?threadid={{ comment['threadid'] }}&replytoid={{ comment['commentid'] }}">Reply</a>
- {% endif %}
- </div>
+<button class="button" onclick="showSettings()" id="btn-show-settings">Settings</button>
+
+<div id="activity-settings" class="boxy" style="line-height: 1.5;" hidden>
+ <!--<span>Push notifications are currently disabled on this device.</span>-->
+ <!--<button onclick="enablePushNotifications()" class="button" style="float: right;">Enable</button>-->
+ Send me a push notification on this device when:
+ <br/>
+ <label><input type="checkbox" onclick="updateSettings()" id="comment-push" {% if comment_push %}checked{% endif %}>I get a new comment</label>
+ <br/>
+ <label><input type="checkbox" onclick="updateSettings()" id="song-push" {% if song_push %}checked{% endif %}>Anyone uploads a new song</label>
+</div>
+
+<br/>
+<br/>
+
+<script>
+function showSettings() {
+ document.getElementById("activity-settings").hidden = false;
+ document.getElementById("btn-show-settings").hidden = true;
+}
+
+async function updateSettings() {
+ const comment_push_enabled = document.getElementById("comment-push").checked;
+ const song_push_enabled = document.getElementById("song-push").checked;
+
+ // Enable/subscribe to notifications
+ if (comment_push_enabled || song_push_enabled)
+ {
+ await enablePushNotifications();
+ }
+
+ // Update notification settings
+ const response = await fetch(
+ "/push-notifications/update-settings", {
+ method: "post",
+ headers: {"Content-Type": "application/json"},
+ body: JSON.stringify({comments: comment_push_enabled, songs: song_push_enabled})
+ }
+ );
+}
+
+async function enablePushNotifications() {
+ if (!("serviceWorker" in navigator))
+ {
+ return; // Browser does not support notifications
+ }
+
+ // Show notification permission request dialog
+ const permission = await window.Notification.requestPermission();
+ if (permission === "granted") {
+ // Subscribe to push notifications (if we don't already have an active subscription)
+ try {
+ const registration = await navigator.serviceWorker.getRegistration();
+ const existingSubscription = await registration.pushManager.getSubscription();
+ if (!existingSubscription)
+ {
+ // Subscribe via browser's push service
+ const vapid_public_key = "BLsO37LostwqKch7SFr5Df0MexEoBOcujdMRY7wJurRPc_MGdz9rAkMrqs_dil4qSFxVbVyAA3FqLEPSL-WRNZs";
+ const options = {userVisibleOnly: true, applicationServerKey: vapid_public_key};
+ const subscription = await registration.pushManager.subscribe(options);
+ console.log(JSON.stringify(subscription));
+
+ // Register subscription with LSP server
+ await fetch(
+ "/push-notifications/subscribe", {
+ method: "post",
+ headers: {"Content-Type": "application/json"},
+ body: JSON.stringify(subscription)
+ }
+ );
+ }
+ }
+ catch (err) {
+ console.log("Error subscribing to push notifications:", err);
+ }
+ }
+ else {
+ console.log("Did not get permission to send notifications:", permission);
+ }
+}
+
+async function notificationsEnabled() {
+ const registration = await navigator.serviceWorker.getRegistration();
+ const permission = await registration.pushManager.permissionState();
+ if (permission == "granted") {
+ const existingSubscription = await registration.pushManager.getSubscription();
+ if (existingSubscription)
+ {
+ return true;
+ }
+ }
+ return false;
+}
+</script>
+
+<div id="activity-contents">
+ {% for comment in comments -%}
+ <div class="boxy">
+ <a href="/users/{{ comment['comment_username'] }}" class="profile-link">{{ comment['comment_username'] }}</a>
+ {% if comment['replyto_content'] %}
+ replied to "{{ comment['replyto_content'] }}"
+ {% else %}
+ commented
+ {% endif %}
+ on
+ {% if 'songid' in comment %}
+ <a href="/song/{{ comment['content_userid'] }}/{{ comment['songid'] }}?action=view">{{ comment['title'] }}</a> -
+ {# Nothing to do for user profile #}
+ {% elif 'playlistid' in comment %}
+ <a href="/playlists/{{ comment['playlistid'] }}">{{ comment['name'] }}</a> -
+ {% elif 'eventid' in comment %}
+ <a href="/jams/{{ comment['jamid']}}/events/{{ comment['eventid'] }}">{{ comment['title'] }}</a> -
+ {% endif %}
+ <a href="/users/{{ comment['content_username'] }}" class="profile-link">{{ comment['content_username'] }}</a>
+ <div class="boxy-lite">
+ <a href="/users/{{ comment['comment_username'] }}" class="profile-link">{{ comment['comment_username'] }}</a>:
+ {{ comment['content'] }}
+
+ <div class="comment-button-container">
+ {% if comment['replytoid'] %}
+ <!-- Comment is already part of a thread; reply to the same thread -->
+ <a href="/comment?threadid={{ comment['threadid'] }}&replytoid={{ comment['replytoid'] }}">Reply</a>
+ {% else %}
+ <!-- Comment is a top-level, reply to the comment -->
+ <a href="/comment?threadid={{ comment['threadid'] }}&replytoid={{ comment['commentid'] }}">Reply</a>
+ {% endif %}
</div>
</div>
+ </div>
{% endfor %}
-{% else %}
-
- Nothing to show here yet!
-
-{% endif %}
+ {% if not comments %} <p>Nothing to show here yet!</p> {% endif %}
+</div>
{% endblock %}
{% endif %}
{% for comment in comments %}
- <div class="top-level-comment">
+ <div class="boxy-lite">
<a href="/users/{{ comment['username'] }}" class="profile-link">{{ comment['username'] }}</a>:
{{ (comment['content'].replace("\n", "<br>"))|safe }}
{% endif %}
{% for reply in comment['replies'] %}
- <div class="reply-comment">
+ <div class="boxy-lite">
<a href="/users/{{ reply['username'] }}" class="profile-link">{{ reply['username'] }}</a>:
- {{ reply['content'] }}
+ {{ (reply['content'].replace("\n", "<br>"))|safe }}
{% if current_userid == reply['userid'] or current_userid == thread_userid %}
<div class="comment-button-container">
{% extends "base.html" %}
-{% block title %}{{ page_title }}{% endblock %}
+{% block title %}Little Song Place{% endblock %}
{% block body %}