from pyhtml import * from html import unescape from bs4 import BeautifulSoup from glom import glom as g from glom import Coalesce from kddit.settings import * from urllib.parse import urlencode from kddit.utils import get_time, human_format, preview_re, external_preview_re, builder, processing_re, video_re, image_re from kddit.utils import tuplefy, get_metadata, replace_tag nothing = (p("there doesn't seem to be anything here"),) style_css = link(rel="stylesheet", type="text/css", href="/static/style.css") slider_css = link(rel="stylesheet", type="text/css", href="/static/slider.css") favicon = link(rel="icon", href="/static/favicon.svg") viewport = meta(name="viewport", content_="width=device-width, initial-scale=1.0") default_head = (style_css, slider_css, favicon, viewport) class progress(Tag): self_closing = False class svg(Tag): self_closing = False class path(Tag): self_closing = False def subreddit_link(sub): return a(Class="sub-link", href=f"/r/{sub}")(f"r/{sub}") def header_div(*args): return div(Class="header")(*args) def container_div(*args): return div(Class="container")(*args) def content_div(*args): return div(Class="content")(*args) def post_div(*args): return div(Class="post")(*args) def inner_post_div(*args): return div(Class="inner-post")(*args) def media_div(*args): return div(Class="media")(*args) def menu_div(*args): return div(Class="menu")(*args) @tuplefy def post_info_div(*args): return div(Class="post-info")(*args) def post_content_div(*args): return div(Class="post-content")(*args) def comment_content_div(*args): return div(Class="comment-content")(*args) def slider(arg): mask = div(Class="css-slider-mask") ul_ = ul(Class="css-slider with-responsive-images") return builder(mask, ul_, arg) def slider_media(arg): slider = li(Class="slide", tabindex=1) outer = span(Class="slide-outer") inner = span(Class="slide-inner") gfx = span(Class="slide-gfx")(arg) return builder(slider, outer, inner, gfx) def nsfw_label(arg): return label(input_(Class="nsfw", type="checkbox"),arg) def get_thumbnail(data): thumbnail = g(data, Coalesce("preview.images.-1.source.url", "secure_media.oembed.thumbnail_url", ), default="") return f"/proxy/{unescape(thumbnail)}" if thumbnail else None def get_video(data): is_gif = g(data, Coalesce("media.reddit_video.is_gif", "preview.reddit_video_preview.is_gif") , default=False) url = g(data, Coalesce("media.reddit_video.fallback_url", "preview.reddit_video_preview.fallback_url", "url")) return f"/video/{url}" if not is_gif else f"/proxy/{url}" @tuplefy def alternate_video(data, url, over_18=False): return None # disabling for now opts = {} opts["src"] = f"/video/{url}" opts["controls"] = "" if nsfw(data) and over_18: opts["preload"] = "none" elif thumbnail := get_thumbnail(data): opts["preload"] = "none" opts["poster"] = thumbnail else: opts["preload"] = "metadata" video_ = media_div(video(**opts)) return video_ def nsfw(data): return data.get("over_18") @tuplefy def reddit_video(data, over_18=False): opts = {"controls":""} opts["preload"] = "none" opts["src"] = get_video(data) if not (nsfw(data) and over_18): opts["poster"] = get_thumbnail(data) video_ = video(**opts) output = media_div(video_) return output @tuplefy def reddit_embed_video(url, over_18=False): opts = {"controls":""} opts["preload"] = "none" if over_18 else "auto" opts["src"] = f'/video/{url}' video_ = video(**opts) output = media_div(video_) return output @tuplefy def reddit_image(data, url=None, over_18=False, text=None): url = url or unescape(g(data, Coalesce("preview.images.-1.variants.gif.source.url", "preview.images.-1.source.url", "url"))) image_ = media_div(img(src=f'/proxy/{url}', loading="lazy"), em(text)) if nsfw(data) and over_18: output = nsfw_label(image_) else: output = image_ return output def gallery(data, over_18=False): output = () images = () for item in reversed(g(data,"gallery_data.items", default=[])): media_id = item["media_id"] url = get_metadata(data, media_id) if url: images += reddit_image(data, url, over_18) if images: output += slider((slider_media(media) for media in images)) return output def page(title_, header_, content_): head_ = head(title(unescape(title_)), default_head) body_ = (header_div(header_), container_div(content_div(content_))) output = html(head_, body_) return output def post_content(data, over_18): output = () text = unescape(data["selftext_html"]) soup = BeautifulSoup(text, "html.parser") for video_link in soup.find_all("a", href=video_re): url = video_link.attrs["href"] name = video_re.match(url).group(1) r_video = reddit_embed_video(f"https://v.redd.it/{name}", over_18=over_18) replace_tag(video_link.parent, r_video) for preview_link in soup.find_all("a", href=preview_re): url = preview_link.attrs["href"] preview_text = preview_link.text caption = preview_text if preview_text != url else None r_image = reddit_image(data, url, over_18, text=caption) replace_tag(preview_link.parent, r_image) for preview_em in soup.find_all("em", string=processing_re): name = processing_re.match(preview_em.text).group(1) if url := get_metadata(data, name): r_image = reddit_image(data, url, over_18) replace_tag(preview_em , r_image) output += (post_content_div(Safe(str(soup))),) return output def comment_content(data, over_18): text = unescape(data["body_html"]) soup = BeautifulSoup(text, "html.parser") for preview_link in soup.find_all("a", href=preview_re): url = preview_link.attrs["href"] preview_text = preview_link.text caption = preview_text if preview_text != url else None r_image = reddit_image(data, url, over_18, text=caption) replace_tag(preview_link, r_image) for image_link in soup.find_all("a", href=image_re): url = image_link.attrs["href"] preview_text = image_link.text caption = preview_text if preview_text != url else None r_image = reddit_image(data, url, over_18, text=caption) replace_tag(image_link, r_image) for preview_img in soup.find_all("img", src=external_preview_re): url = preview_img.attrs["src"] preview_img.attrs["src"] = f'/proxy/{url}' for preview_em in soup.find_all("em", string=processing_re): name = processing_re.match(preview_em.text).group(1) if url := get_metadata(data, name): r_image = reddit_image(data, url, over_18) replace_tag(preview_em , r_image) return builder(comment_content_div, Safe,str,soup) @tuplefy def subreddit_menu(option, subreddit): output = [] focused = option or DEFAULT_OPTION for o in SUBREDDIT_OPTIONS: focus = o == focused sub = f"/r/{subreddit}" if subreddit else "" url = f"{sub}/{o}" a_ = a(href=url, Class="focus")(o) if focus else a(href=url)(o) output.append(a_) return menu_div(output) @tuplefy def search_sort_menu(subreddit, params): output = [] focused = params.get("sort", "relevance") for o in SEARCH_SORT: query = params.copy() query["sort"] = o focus = o == focused sub = f"/r/{subreddit}" if subreddit else "" url = f"{sub}/search?{urlencode(query)}" a_ = a(href=url, Class="focus")(o) if focus else a(href=url)(o) output.append(a_) return menu_div(output) @tuplefy def search_time_menu(subreddit, params): output = [] focused = params.get("t", "hour") for i, v in TIME_OPTIONS.items(): query = params.copy() query["t"] = i focus = i == focused sub = f"/r/{subreddit}" if subreddit else "" url = f"{sub}/search?{urlencode(query)}" a_ = a(Class="focus",href=url)(v) if focus else a(href=url)(v) output.append(a_) return menu_div(output) @tuplefy def domain_menu(option, domain): output = [] focused = option or DEFAULT_OPTION for o in SUBREDDIT_OPTIONS: focus = o == focused url = f"/domain/{domain}/{o}" a_ = a(href=url, Class="focus")(o) if focus else a(href=url)(o) output.append(a_) return menu_div(output) @tuplefy def subreddit_sort_menu(subreddit, option, time=None): p = f"/r/{subreddit}" if subreddit else "" focused = time or "hour" output = [] for i, v in TIME_OPTIONS.items(): focus = i == focused url = f'{p}/{option}?t={i}' a_ = a(Class="focus",href=url)(v) if focus else a(href=url)(v) output.append(a_) return menu_div(output) @tuplefy def domain_sort_menu(domain, option, time=None): output = [] focused = time or "hour" for i, v in TIME_OPTIONS.items(): focus = i == focused url = f"/domain/{domain}/{option}?t={i}" a_ = a(Class="focus",href=url)(v) if focus else a(href=url)(v) output.append(a_) return menu_div(output) @tuplefy def multi_sort_menu(user, multi, option, time=None): p = f"/u/{user}/m/{multi}" focused = time or "hour" output = [] for i, v in TIME_OPTIONS.items(): focus = i == focused url = f'{p}/{option}?t={i}' a_ = a(Class="focus",href=url)(v) if focus else a(href=url)(v) output.append(a_) return menu_div(output) @tuplefy def user_menu(option, user): output = [] for o in USER_OPTIONS: focus = option == o or (not option and o == DEFAULT_OPTION) link_ = f"/u/{user}/{o}" if focus: a_ = a(href=link_, Class="focus")(o) else: a_ = a(href=link_)(o) output.append(a_) return menu_div(output) @tuplefy def user_sort_menu(option, sort, user): output = [] focused = sort or DEFAULT_OPTION for o in USER_SORT: focus = o == focused link_ = f"/u/{user}/{option}/?sort={o}" a_ = a(href=link_, Class="focus")(o) if focus else a(href=link_)(o) output.append(a_) return menu_div(output) @tuplefy def user_comments_sort_menu(path, sort): output = [] focused = sort or DEFAULT_OPTION for o in USER_COMMENT_SORT: focus = o == focused link_ = f"{path}/?sort={o}" a_ = a(href=link_, Class="focus")(o) if focus else a(href=link_)(o) output.append(a_) return menu_div(output) @tuplefy def multi_menu(option, user, multi): output = [] for o in SUBREDDIT_OPTIONS: focus = option == o or (not option and o == DEFAULT_OPTION) link_ = f"/u/{user}/m/{multi}/{o}" if focus: a_ = a(href=link_, Class="focus")(o) else: a_ = a(href=link_)(o) output.append(a_) return menu_div(output) @tuplefy def before_link(data, target, option, t=None): option = option or "" sub = f"/{target}" if target else "" time = f"t={t}&" if t else "" url = f'{sub}/{option}?{time}count=25&before={data["data"]["before"]}' a_ = a(Class="button", href=url)("") return a_ @tuplefy def search_before_link(data, target, params): query = params.copy() query.pop("after", None) query["before"] = g(data,"data.before") url = f'{target}/?{urlencode(query)}' a_ = a(Class="button", href=url)("") return a_ @tuplefy def user_before_link(data, target, option, sort=None): option = option or "" sub = f"/{target}" if target else "" time = f"sort={sort}&" if sort else "" url = f'{sub}/{option}?{time}count=25&before={data["data"]["before"]}' a_ = a(Class="button", href=url)("") return a_ def reddit_media(data, over_18): output = () if data["is_video"] or g(data, "preview.reddit_video_preview", default=None): output += reddit_video(data, over_18=over_18) elif (data.get("post_hint") and data.get("post_hint") != "image") or not data.get('is_reddit_media_domain'): return output else: output += reddit_image(data, over_18=over_18) return post_content_div(output) def reddit_content(data, over_18=False): if data.get("is_gallery"): output = gallery(data, over_18=over_18) elif not data.get("is_self") and (data.get("thumbnail") and data.get("thumbnail") not in ("self", "spoiler")) or data.get("is_reddit_media_domain"): output = reddit_media(data, over_18) else: output = None return output def rich_text(richtext, text): for item in richtext: a_ = item.get("a") u = item.get("u") if not (a_ or u): continue text = text.replace(a_, f'') return text def domain_link(data): if data.get("is_self"): return None elif data.get("author") == "[deleted]": return None elif data.get("crosspost_parent_list"): return None domain = data.get("domain") domain_url = f"/domain/{domain}" return ("(", a(href=domain_url)(f"{domain}"), ")") @tuplefy def post(data, over_18=False, from_user=False): content = () if not data.get("is_self") and not data.get("crosspost_parent_list"): content += (a(Class="post-link",href=data["url"])(data["url"]),) if data.get("selftext_html"): content += post_content(data, over_18) if data.get("crosspost_parent_list"): content += post(data['crosspost_parent_list'][0], True) elif data.get("poll_data"): content += poll(data) elif data.get("removed_by_category") or (data.get("author") == "[deleted]"): pass elif result := reddit_content(data, over_18): content += (result,) author = data.get("author") permalink = data.get("permalink") title_ = unescape(data.get("title")) domain = domain_link(data) votes = human_format(int(data.get("ups") or data.get("downs"))) author_info = ("Posted by", a(href=f'/u/{author}')(f'u/{author}')) title_link = builder(a(href=permalink),Safe,b,title_) info_args = (subreddit_link(data["subreddit"]),"โ€ข", author_info, get_time(data["created"]), domain) if from_user: user_comment_url = f"/user/{author}/comments/{data['id']}/_" info_args += (a(href=user_comment_url)("๐Ÿ”—"),) post_info = post_info_div(*info_args) flair = post_flair(data) inner = (title_link, flair, content) votes = div(Class="votes")( span(Class="icon icon-upvote"), votes, ) return post_div(votes, inner_post_div(post_info, inner)) @tuplefy def poll(data): poll_options = () tvotes = g(data,"poll_data.total_vote_count") for opt in data["poll_data"]["options"]: if "vote_count" in opt: votes = opt["vote_count"] cin = ( p(f'{opt["text"]} : {votes}'), progress( value=votes, max=tvotes)) poll_options += cin else: cin = (p(input_(disabled="", type="radio"), opt["text"])) poll_options += (cin,) div_ = div(Class="poll")(poll_options) return div_ def posts(data, over_18=False): posts_ = () for children in g(data, "data.children"): data = children["data"] posts_ += post(data, over_18) return posts_ @tuplefy def mixed_content(data, over_18, from_user = False): output = () for children in g(data, "data.children"): if children["kind"] == "t1": output += (comment(children, False, from_user),) elif children["kind"] == "t3": output += (post(children["data"], over_18, from_user),) return output def comment_flair(data): flair_text = g(data, "author_flair_text", default=None) if flair_richtext := data.get("author_flair_richtext"): flair_text = rich_text(flair_richtext, flair_text ) return builder(span(Class="flair"),Safe,unescape,flair_text) if flair_text else None def post_flair(data): flair_text = g(data, "link_flair_text", default=None) if flair_richtext := data.get("link_flair_richtext"): flair_text = rich_text(flair_richtext, flair_text ) return builder(span(Class="flair"),Safe,unescape,flair_text) if flair_text else None def comment(data, full=False, from_user=False): comment_ = data["data"] flair = comment_flair(comment_) if full: title_ = comment_["link_title"] header_ = () header_ += ("by", a(href=f'/u/{comment_["author"]}')(f'u/{comment_["author"]}'),flair) header_ += ("in", subreddit_link(comment_["subreddit"])) header_ += (get_time(comment_["created"]),) if from_user: user_comment_url = f"/u/{comment_['author']}/comments/{data['id']}/comment/{comment_['id']}" header_ += a(href=user_comment_url)("๐Ÿ”—") inner = ( a(href=comment_["permalink"])(b(title_)), div(Class="comment-info")(header_), comment_content(comment_, True) ) return div(Class="comment")(inner) else: replies_ = replies(data) a_ = a(href=f'/u/{comment_["author"]}')(f'u/{comment_["author"]}') link_ = a(href=comment_["permalink"])("๐Ÿ”—") points = (span(human_format(int(comment_["ups"] or comment_["downs"]))), "points", "ยท" ) inner = (div(Class="comment-info")( a_,flair, points, get_time(comment_["created"]), link_), comment_content(comment_, True), replies_) return div(Class="comment")(inner) @tuplefy def reply(data): comment_ = data["data"] flair = comment_flair(comment_) replies_ = replies(data) a_ = a(href=f'/u/{comment_["author"]}')(f'u/{comment_["author"]}') link_ = a(href=comment_["permalink"])("๐Ÿ”—") points = (span(human_format(int(comment_["ups"] or comment_["downs"]))), "points", "ยท" ) inner = (div(Class="comment-info")( a_,flair, points, get_time(comment_["created"]), link_), comment_content(comment_, True), replies_) return div(Class="reply")(inner) @tuplefy def comments(data_list, from_user=False): comments = () for data in data_list: if data['kind'] == "more": comments += (p("..."),) else: comments += (comment(data, False, from_user),) return div(Class="comments")(comments) def replies(data): replies_ = () if data['kind'] == "more": replies_ += (p("..."),) for children in g(data, "data.replies.data.children", default=[]): if children['kind'] == "more": replies_ += (p("..."),) else: replies_ += reply(children) return ul(replies_) if replies else None @tuplefy def subreddit_nav(data, subreddit, option=None, time=None): buttons = () target = f"r/{subreddit}" if subreddit else "" if data["data"]["before"]: buttons += before_link(data, target, option, time) if data["data"]["after"]: buttons += after_link(data, target, option, time) return div(Class="nav")(buttons) if buttons else () @tuplefy def search_nav(data, subreddit, params): buttons = () target = f"/r/{subreddit}/search" if subreddit else "/search" if g(data, "data.before"): buttons += search_before_link(data, target, params) if g(data, "data.after"): buttons += search_after_link(data, target, params) return div(Class="nav")(buttons) if buttons else None @tuplefy def domain_nav(data, domain, option=None, time=None): buttons = () target = f"domain/{domain}" if data["data"]["before"]: buttons += before_link(data, target, option, time) if data["data"]["after"]: buttons += after_link(data, target, option, time) return div(Class="nav")(buttons) if buttons else () @tuplefy def user_nav(data, user, option=None, time=None): buttons = () target = f"u/{user}" if data["data"]["before"]: buttons += user_before_link(data, target, option, time) if data["data"]["after"]: buttons += user_after_link(data, target, option, time) return div(Class="nav")(buttons) if buttons else () @tuplefy def multi_nav(data, user, multi, option=None, time=None): buttons = () target = f"u/{user}/m/{multi}" if data["data"]["before"]: buttons += user_before_link(data, target, option, time) if data["data"]["after"]: buttons += user_after_link(data, target, option, time) return div(Class="nav")(buttons) if buttons else () def page_header(subreddit=None, user=None, multi=None, domain=None): header_ = (a(Class="main-link",href="/")("kddit"),) if subreddit: header_ += (a(Class="subreddit-link", href=f"/r/{subreddit}")(f"r/{subreddit}"),) elif multi and user: header_ += (a(Class="subreddit-link", href=f"/u/{user}/m/{multi}")(f"u/{user}/m/{multi}"),) elif user: header_ += (a(Class="subreddit-link", href=f"/u/{user}")(f"u/{user}"),) elif domain: header_ += (a(Class="subreddit-link", href=f"/domain/{domain}")(f"domain/{domain}"),) return header_ def error_page(error): title_ = f"{error.status}!" output = h1(title_) header_ = page_header() return page(title_, header_, output)