=> 374faca7bee892540231982b2b3ab5d36419d3a1
[1mdiff --git a/50_bubble.py b/50_bubble.py[m [1mindex e29a0b7..039def0 100644[m [1m--- a/50_bubble.py[m [1m+++ b/50_bubble.py[m [36m@@ -109,10 +109,15 @@[m [mBubble is open source:[m self.token = None[m self.notif_count = None[m self.is_short_preview = False[m [32m+[m[32m self.tz = pytz.timezone('UTC')[m [m def set_user(self, user):[m self.user = user[m if user:[m [32m+[m[32m try:[m [32m+[m[32m self.tz = pytz.timezone(user.timezone)[m [32m+[m[32m except:[m [32m+[m[32m pass[m if user.flags & User.ASCII_ICONS_FLAG:[m self.CHECKS = [ '[_]', '[x]' ][m # TODO: Add more of these.[m [36m@@ -213,7 +218,7 @@[m [mBubble is open source:[m cmt = 'View post' if post.num_cmts == 0 and is_user_post else \[m '' if post.num_cmts == 0 else \[m f'{post.num_cmts} comment{plural_s(post.num_cmts)}'[m [31m- age = post.age()[m [32m+[m[32m age = post.age(tz=self.tz)[m bell = ' 馃敂' if post.num_notifs else ''[m [m author = '馃寬 ' + sub if sub else (post.poster_avatar + ' ' + post.poster_name)[m [1mdiff --git a/db-migrate.sql b/db-migrate.sql[m [1mindex 9262e7a..391761b 100644[m [1m--- a/db-migrate.sql[m [1m+++ b/db-migrate.sql[m [36m@@ -20,4 +20,5 @@[m [mALTER TABLE posts ADD INDEX (user);[m ALTER TABLE posts ADD INDEX (issueid);[m [m -- Migration from v4 to v5 --[m [32m+[m[32mALTER TABLE users ADD COLUMN timezone VARCHAR(40) DEFAULT 'UTC';[m ALTER TABLE users ADD COLUMN recovery VARCHAR(1000) DEFAULT '';[m [1mdiff --git a/model.py b/model.py[m [1mindex 7cb3ca2..c45808d 100644[m [1m--- a/model.py[m [1m+++ b/model.py[m [36m@@ -96,14 +96,16 @@[m [mclass Notification:[m self.subname = subname[m self.reaction = reaction[m [m [31m- def ymd_date(self, fmt='%Y-%m-%d'):[m [32m+[m[32m def ymd_date(self, fmt='%Y-%m-%d', tz=None):[m dt = datetime.datetime.fromtimestamp(self.ts, UTC)[m [32m+[m[32m if tz:[m [32m+[m[32m dt = dt.astimezone(tz)[m return dt.strftime(fmt)[m [m def age(self):[m return ago_text(self.ts)[m [m [31m- def entry(self, show_age=True, with_time=False, with_title=True) -> tuple:[m [32m+[m[32m def entry(self, show_age=True, with_time=False, with_title=True, tz=None) -> tuple:[m """Returns (link, label) to use in the notification list."""[m [m event = ''[m [36m@@ -170,7 +172,7 @@[m [mclass Notification:[m event += f' "{vis_title}"'[m [m age = f' 路 {self.age()}' if show_age else ''[m [31m- hm_time = f" at {self.ymd_date('%H:%M')}" if with_time else ''[m [32m+[m[32m hm_time = f" at {self.ymd_date('%H:%M', tz)}" if with_time else ''[m return f'/notif/{self.id}', f'{icon}{self.src_name} {event}{hm_time}{age}'[m [m [m [36m@@ -193,7 +195,8 @@[m [mclass User:[m SHORT_PREVIEW_FLAG = 0x0020[m [m def __init__(self, id, name, info, url, recovery, avatar, role, flags, notif, email, email_inter, \[m [31m- email_range, password, ts_password, ts_created, ts_active, sort_post, sort_cmt):[m [32m+[m[32m email_range, password, ts_password, ts_created, ts_active, sort_post, sort_cmt,[m [32m+[m[32m timezone):[m self.id = id[m self.name = name[m self.info = info[m [36m@@ -212,6 +215,7 @@[m [mclass User:[m self.ts_active = ts_active[m self.sort_post = sort_post[m self.sort_cmt = sort_cmt[m [32m+[m[32m self.timezone = timezone[m [m def subspace_link(self, prefix=''):[m return f'=> /u/{self.name} {self.avatar} {prefix}u/{self.name}\n'[m [36m@@ -265,8 +269,10 @@[m [mclass Commit:[m self.msg = msg[m self.ts = ts[m [m [31m- def ymd_date(self):[m [32m+[m[32m def ymd_date(self, tz=None):[m dt = datetime.datetime.fromtimestamp(self.ts, UTC)[m [32m+[m[32m if tz:[m [32m+[m[32m dt = dt.astimezone(tz)[m return dt.strftime('%Y-%m-%d')[m [m def entry(self, view_url, outgoing=False):[m [36m@@ -320,19 +326,23 @@[m [mclass Post:[m else:[m return '(untitled issue)' if self.issueid else '(untitled post)'[m [m [31m- def ymd_date(self):[m [32m+[m[32m def ymd_date(self, tz=None):[m dt = datetime.datetime.fromtimestamp(self.ts_created, UTC)[m [32m+[m[32m if tz:[m [32m+[m[32m dt = dt.astimezone(tz)[m return dt.strftime('%Y-%m-%d')[m [m [31m- def ymd_hm_tz(self):[m [32m+[m[32m def ymd_hm_tz(self, tz=None):[m dt = datetime.datetime.fromtimestamp(self.ts_created, UTC)[m [31m- return dt.strftime('%Y-%m-%d %H:%M UTC')[m [32m+[m[32m if tz:[m [32m+[m[32m dt = dt.astimezone(tz)[m [32m+[m[32m return dt.strftime(f'%Y-%m-%d %H:%M %z')[m [m [31m- def age(self):[m [32m+[m[32m def age(self, tz=None):[m if self.is_draft:[m return 'Now'[m else:[m [31m- return ago_text(self.ts_created)[m [32m+[m[32m return ago_text(self.ts_created, tz=tz)[m [m def page_url(self):[m if self.sub_owner:[m [36m@@ -412,7 +422,8 @@[m [mclass Database:[m ts_active TIMESTAMP DEFAULT CURRENT_TIMESTAMP,[m ts_email TIMESTAMP DEFAULT '2000-01-01 00:00:00',[m sort_post CHAR(1) DEFAULT 'r',[m [31m- sort_cmt CHAR(1) DEFAULT 'n'[m [32m+[m[32m sort_cmt CHAR(1) DEFAULT 'n',[m [32m+[m[32m timezone VARCHAR(40) DEFAULT 'UTC'[m )""", (0xffff,))[m [m db.execute("""CREATE TABLE IF NOT EXISTS certs ([m [36m@@ -662,6 +673,7 @@[m [mclass Database:[m UNIX_TIMESTAMP(u.ts_active),[m u.sort_post,[m u.sort_cmt,[m [32m+[m[32m u.timezone,[m s.id[m FROM users u[m JOIN subspaces s ON s.owner=u.id[m [36m@@ -669,10 +681,12 @@[m [mclass Database:[m [m for (id, name, info, url, recovery, avatar, role, flags, notif, email, email_inter,[m email_range, password, ts_password, ts_created, ts_active,[m [31m- sort_post, sort_cmt, user_subspace_id) in cur:[m [32m+[m[32m sort_post, sort_cmt, timezone, user_subspace_id) in cur:[m user = User(id, name, info, url, recovery, avatar, role, flags, notif,[m [31m- email, email_inter, email_range, password, \[m [31m- ts_password, ts_created, ts_active, sort_post, sort_cmt)[m [32m+[m[32m email, email_inter, email_range,[m [32m+[m[32m password, ts_password,[m [32m+[m[32m ts_created, ts_active,[m [32m+[m[32m sort_post, sort_cmt, timezone)[m user.moderated_subspace_ids = [user_subspace_id] + self.get_moderated_subspace_ids(user)[m return user[m [m [36m@@ -721,7 +735,7 @@[m [mclass Database:[m email=None, email_inter=None, email_range=None,[m notif=None, flags=None,[m password=None, password_expiration_offset_minutes=0,[m [31m- sort_post=None, sort_cmt=None,[m [32m+[m[32m sort_post=None, sort_cmt=None, timezone=None,[m active=False):[m if type(user) is User:[m user = user.id[m [36m@@ -768,6 +782,9 @@[m [mclass Database:[m if sort_cmt:[m stm.append('sort_cmt=?')[m values.append(sort_cmt)[m [32m+[m[32m if timezone:[m [32m+[m[32m stm.append('timezone=?')[m [32m+[m[32m values.append(timezone)[m if active:[m stm.append('ts_active=CURRENT_TIMESTAMP()')[m if stm:[m [36m@@ -1045,7 +1062,7 @@[m [mclass Database:[m [m mods = [][m for (id, avatar, name) in cur:[m [31m- mods.append(User(id, name, None, None, None, avatar, None, None, None, None, None, None,[m [32m+[m[32m mods.append(User(id, name, None, None, None, avatar, None, None, None, None, None, None, None,[m None, None, None, None, None, None))[m return mods[m [m [36m@@ -2261,7 +2278,7 @@[m [mclass Search:[m for (ts, id, name, avatar, info, url) in cur:[m self.results.append(((exact_match(name), ts),[m User(id, name, info, url, None, avatar, None, None, None,[m [31m- None, None, None, None, None, ts, None, None, None)))[m [32m+[m[32m None, None, None, None, None, ts, None, None, None, None)))[m [m # Subspaces.[m cur.execute(f"""[m [1mdiff --git a/requirements.txt b/requirements.txt[m [1mnew file mode 100644[m [1mindex 0000000..af44f19[m [1m--- /dev/null[m [1m+++ b/requirements.txt[m [36m@@ -0,0 +1 @@[m [32m+[m[32mpytz[m [1mdiff --git a/settings.py b/settings.py[m [1mindex 2fb5b76..4766529 100644[m [1m--- a/settings.py[m [1m+++ b/settings.py[m [36m@@ -1,4 +1,5 @@[m import math[m [32m+[m[32mimport pytz[m from utils import *[m from model import Notification, User, Subspace, FOLLOW_USER, FOLLOW_SUBSPACE, \[m MUTE_USER, MUTE_SUBSPACE[m [36m@@ -176,7 +177,7 @@[m [mdef make_settings_page(session):[m return 30, '/settings/notif'[m [m elif req.path == session.path + 'settings/email-range':[m [31m- prompt = 'Hour range when emails are not sent, in UTC: (Examples: "0-6", "21-3")'[m [32m+[m[32m prompt = f'{session.tz.localize(datetime.datetime.now()).tzname()} hour range (inclusive) when emails are not sent: (Examples: "0-6", "21-3")'[m pattern = re.compile(r'(\d+)-(\d+)')[m if req.query == None:[m return 10, prompt[m [36m@@ -188,6 +189,12 @@[m [mdef make_settings_page(session):[m begin = min(23, begin)[m end = max(0, end)[m end = min(23, end)[m [32m+[m[32m # Convert from user's time zone to UTC. The actual date doesn't matter,[m [32m+[m[32m # we are just using the hours.[m [32m+[m[32m dt_begin = session.tz.localize(datetime.datetime(2023, 6, 19, begin, 0, 0))[m [32m+[m[32m dt_end = session.tz.localize(datetime.datetime(2023, 6, 19, end, 0, 0))[m [32m+[m[32m begin = dt_begin.astimezone(pytz.utc).hour[m [32m+[m[32m end = dt_end.astimezone(pytz.utc).hour[m db.update_user(session.user, email_range=f"{begin}-{end}")[m return 30, '/settings/notif'[m [m [36m@@ -287,7 +294,20 @@[m [mdef make_settings_page(session):[m page += '\n### Email\n'[m page += f'=> /settings/email 馃摟 Send to: {session.user.email if session.user.email else "(not set)"}\n'[m page += f'\n=> /settings/email-inter Interval: {session.user.email_inter} minutes\n'[m [31m- email_range = "(not set)" if not session.user.email_range else session.user.email_range[m [32m+[m[32m #email_range = "(not set)" if not session.user.email_range else session.user.email_range[m [32m+[m [32m+[m[32m # Convert the range to local hours.[m [32m+[m[32m if session.user.email_range:[m [32m+[m[32m begin, end = map(int, session.user.email_range.split('-'))[m [32m+[m[32m # The date doesn't matter, we are just using the hours.[m [32m+[m[32m dt_begin = datetime.datetime(2023, 6, 19, begin, 0, 0, tzinfo=UTC)[m [32m+[m[32m dt_end = datetime.datetime(2023, 6, 19, end, 0, 0, tzinfo=UTC)[m [32m+[m[32m dt_begin = dt_begin.astimezone(session.tz)[m [32m+[m[32m dt_end = dt_end.astimezone(session.tz)[m [32m+[m[32m email_range = f"{dt_begin.hour}-{dt_end.hour}"[m [32m+[m[32m else:[m [32m+[m[32m email_range = "(not set)"[m [32m+[m page += f'=> /settings/email-range "Do not disturb" range: {email_range}\n'[m [m return page[m [36m@@ -507,6 +527,18 @@[m [mdef make_settings_page(session):[m page += '\n'[m return page[m [m [32m+[m[32m elif req.path == session.path + 'settings/timezone':[m [32m+[m[32m if is_empty_query(req):[m [32m+[m[32m page = 'Select a time zone:\n\n'[m [32m+[m[32m for tz in pytz.all_timezones:[m [32m+[m[32m page += f'=> ?{tz} {tz}\n'[m [32m+[m[32m return page[m [32m+[m[32m tz = clean_query(req)[m [32m+[m[32m if not tz in pytz.all_timezones:[m [32m+[m[32m return 50, 'Invalid time zone'[m [32m+[m[32m db.update_user(session.user, timezone=tz)[m [32m+[m[32m return 30, '/settings/display'[m [32m+[m elif req.path == session.path + 'settings/display':[m page += f'# {session.user.name}: Settings\n'[m page += '=> /settings 鈿欙笍 Go back\n\n'[m [36m@@ -515,10 +547,12 @@[m [mdef make_settings_page(session):[m [m page += '## Display\n\n'[m page += f'=> /settings/short-preview {session.CHECKS[nonzero(session.user.flags & User.SHORT_PREVIEW_FLAG)]} Short post previews\n'[m [32m+[m[32m page += f'\n=> /settings/timezone Time zone: {user.timezone}\n'[m page += f'=> /settings/ascii Display icons as: {ICON_MODE[nonzero(session.user.flags & User.ASCII_ICONS_FLAG)]}\n'[m return page[m [m [31m- elif req.path == session.path + 'settings':[m [32m+[m[32m elif req.path == session.path + 'settings' or \[m [32m+[m[32m req.path == session.path + 'settings/':[m page = f'# {session.user.name}: Settings\n'[m page += f'\n=> /dashboard Back to Dashboard\n'[m page += f'=> / Back to front page\n\n'[m [1mdiff --git a/subspace.py b/subspace.py[m [1mindex 9afc67a..bff379d 100644[m [1m--- a/subspace.py[m [1m+++ b/subspace.py[m [36m@@ -329,7 +329,7 @@[m [mdef make_search_page(session):[m kind = "Comment" if obj.parent else f"Issue #{obj.issueid}" if obj.issueid else "Post"[m title = f' "{shorten_text(obj.title, 30)}"' if obj.title else ''[m scope_desc = f"in {ctx} " if not scope and not obj.sub_owner else ""[m [31m- page += f'=> /{ctx}/{obj.id} {kind}{title} {scope_desc}by {obj.poster_avatar} {obj.poster_name} on {obj.ymd_date()} {" 路 " if obj.tags else ""}{obj.tags}\n'[m [32m+[m[32m page += f'=> /{ctx}/{obj.id} {kind}{title} {scope_desc}by {obj.poster_avatar} {obj.poster_name} on {obj.ymd_date(tz=session.tz)} {" 路 " if obj.tags else ""}{obj.tags}\n'[m SEGTYPES = ['content', 'URL', 'image', 'attachment', 'poll option'][m if result[2] != Segment.TEXT:[m page += f'(matching {SEGTYPES[result[2]]}) '[m [1mdiff --git a/user.py b/user.py[m [1mindex 12ad553..29490aa 100644[m [1m--- a/user.py[m [1m+++ b/user.py[m [36m@@ -152,8 +152,8 @@[m [mdef user_actions(session):[m notifs = db.get_notifications(session.user, include_hidden=True, sort_desc=True)[m cur_ymd = None[m for notif in notifs:[m [31m- ymd = notif.ymd_date()[m [31m- link, label = notif.entry(show_age=False, with_time=True)[m [32m+[m[32m ymd = notif.ymd_date(tz=session.tz)[m [32m+[m[32m link, label = notif.entry(show_age=False, with_time=True, tz=session.tz)[m if cur_ymd != ymd:[m cur_ymd = ymd[m page += f'## {ymd}\n\n'[m [36m@@ -168,7 +168,7 @@[m [mdef user_actions(session):[m page += '\nNo activity.\n'[m for notif in notifs:[m link, label = notif.entry(show_age=False)[m [31m- page += f'=> {link} {notif.ymd_date()} {label}\n'[m [32m+[m[32m page += f'=> {link} {notif.ymd_date(tz=session.tz)} {label}\n'[m return page[m [m notif = db.get_notification(session.user, notif_id, clear=True)[m [36m@@ -242,7 +242,7 @@[m [mdef make_dashboard_page(session):[m sub = f' 路 s/{post.sub_name}'[m else:[m sub = ''[m [31m- page += f'=> /edit/{post.id} {post.ymd_date()} {post.title_text()}{sub}\n'[m [32m+[m[32m page += f'=> /edit/{post.id} {post.ymd_date(tz=session.tz)} {post.title_text()}{sub}\n'[m [m subs = db.get_subspaces(mod=session.user.id)[m n = len(subs)[m [1mdiff --git a/utils.py b/utils.py[m [1mindex c0b883c..1b8c5bb 100644[m [1m--- a/utils.py[m [1m+++ b/utils.py[m [36m@@ -169,7 +169,8 @@[m [mdef shorten_text(text, n):[m [m [m def time_delta_text(sec, date_ts, suffix='ago', now='Now',[m [31m- date_prefix='', date_fmt='%Y-%m-%d', date_sep=' 路 '):[m [32m+[m[32m date_prefix='', date_fmt='%Y-%m-%d', date_sep=' 路 ',[m [32m+[m[32m tz=None):[m if sec < 2:[m return now[m if sec < 60:[m [36m@@ -182,6 +183,8 @@[m [mdef time_delta_text(sec, date_ts, suffix='ago', now='Now',[m return f'{hours} hour{plural_s(hours)} {suffix}'[m days = round(sec / 3600 / 24)[m dt = datetime.datetime.fromtimestamp(date_ts, UTC)[m [32m+[m[32m if tz:[m [32m+[m[32m dt = dt.astimezone(tz)[m age = date_prefix + dt.strftime(date_fmt)[m if days < 14:[m return age + f'{date_sep}{days} day{plural_s(days)} {suffix}'[m [36m@@ -195,9 +198,9 @@[m [mdef time_delta_text(sec, date_ts, suffix='ago', now='Now',[m return age + f'{date_sep}{years} year{plural_s(years)} {suffix}'[m [m [m [31m-def ago_text(ts, suffix='ago', now='Now'):[m [32m+[m[32mdef ago_text(ts, suffix='ago', now='Now', tz=None):[m sec = max(0, int(time.time()) - ts)[m [31m- return time_delta_text(sec, ts, suffix, now)[m [32m+[m[32m return time_delta_text(sec, ts, suffix, now, tz=tz)[m [m [m def is_empty_query(req):[m [1mdiff --git a/worker.py b/worker.py[m [1mindex 7773beb..0af06cf 100644[m [1m--- a/worker.py[m [1m+++ b/worker.py[m [36m@@ -69,7 +69,7 @@[m [mclass Emailer (threading.Thread):[m pending_notifs.append(User(id, name, None, None, None, None, None,[m None, enabled_types, email, None,[m email_range, None, None, None, None,[m [31m- None, None))[m [32m+[m[32m None, None, None))[m [m messages = {}[m footer = \[m
text/gemini; charset=utf-8
This content has been proxied by September (ba2dc).