$source.content
', + 'tag': [], + 'source': {'mediaType': 'text/markdown', 'content': text}, + 'published': '$NOW', + 'temp_uuid': "$UUID", + 'id': '$DOMAIN/outbox/$temp_uuid/activity', + 'url': '$DOMAIN/note/$temp_uuid', + }) + create = manager.Create( + **{ + 'context': ['https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + {'Hashtag': 'as:Hashtag', + 'sensitive': 'as:sensitive', + 'toot': 'http://joinmastodon.org/ns#', + 'featured': 'toot:featured'}], + 'actor': '$DOMAIN', + 'object': note.to_dict(), + 'published': '$NOW', + 'to': ['https://www.w3.org/ns/activitystreams#Public'], + 'cc': ['$DOMAIN/followers'], + 'id': '$DOMAIN/outbox/%s' % note.temp_uuid, + } + ) + message = manager.Create( + **{ + 'activity': create.to_dict(), + 'box': 'outbox', + 'type': ['Create'], + 'remote_id': '$DOMAIN/outbox/%s' % note.temp_uuid, + 'meta': {'undo': False, 'deleted': False}, + }) + + database.activities.insert_one(message.to_dict()) +""" + +manager = Manager(database=database) +manager.setup_css() +## FIXME: get rid of all of these: +manager.config.update({ + "ME": { + "url": "https://example.com", + "icon": {"url": "https://example.com"}, + "icon_url": 'https://cs.brynmawr.edu/~dblank/images/doug-sm-orig.jpg', + "summary": "I'm just me."}, + "NAME": "ActivityPub Blog", + "ID": "http://localhost:%s/dsblank" % manager.port, + "BASE_URL": "http://localhost:%s" % manager.port, + }) + +#### The routes: + +@app.route("/notes", endpoint="notes") +@app.route("/") +def route_index(self, *args, **kwargs): + logging.info("args: %s, kwargs: %s" % (args, kwargs)) + q = { + "box": "outbox", + "type": {"$in": ["Create", "Announce"]}, + "activity.object.inReplyTo": None, + "meta.deleted": False, + "meta.undo": False, + } + outbox_data, older_than, newer_than = paginated_query(self, self.database.activities, q) + logging.info("outbox_data: %s" % outbox_data) + return self.render_template( + "index.html", + outbox_data=outbox_data, + older_than=older_than, + newer_than=newer_than, + ) + +@app.route("/admin", methods=["GET"]) +#@login_required +def route_admin(self): + q = { + "meta.deleted": False, + "meta.undo": False, + "type": "like", + "box": "outbox", + } + col_liked = self.database.activities.count(q) + + return self.render_template( + "admin.html", + instances=list(self.database.instances.find()), + inbox_size=self.database.activities.count({"box": "inbox"}), + outbox_size=self.database.activities.count({"box": "outbox"}), + col_liked=col_liked, + col_followers=self.database.activities.count( + { + "box": "inbox", + "type": "follow", + "meta.undo": False, + } + ), + col_following=self.database.activities.count( + { + "box": "outbox", + "type": "follow", + "meta.undo": False, + } + ), + ) + +@app.route("/login", methods=["POST", "GET"]) +def route_login(self): + return self.redirect( + self.get_argument("redirect", None) or self.url_for("admin_notifications") + ) + +@app.route("/admin/notifications") +def admin_notifications(self): + # FIXME(tsileo): show unfollow (performed by the current actor) and liked??? + mentions_query = { + "type": "Create", + "activity.object.tag.type": "Mention", + "activity.object.tag.name": "@dsblank@https://example.com", + "meta.deleted": False, + } + replies_query = { + "type": "Create", + "activity.object.inReplyTo": {"$regex": "^https://example.com"}, + } + announced_query = { + "type": "Announce", + "activity.object": {"$regex": "^https://example.com"}, + } + new_followers_query = {"type": "Follow"} + unfollow_query = { + "type": "Undo", + "activity.object.type": "Follow", + } + followed_query = {"type": "Accept"} + q = { + "box": "inbox", + "$or": [ + mentions_query, + announced_query, + replies_query, + new_followers_query, + followed_query, + unfollow_query, + ], + } + inbox_data, older_than, newer_than = paginated_query(self, self.database.activities, q) + + return self.render_template( + "stream.html", + inbox_data=inbox_data, + older_than=older_than, + newer_than=newer_than, + ) + +### FIXME: move paging to Manager +def paginated_query(self, db, q, limit=5, sort_key="_id"): + older_than = newer_than = None + query_sort = -1 + first_page = (not self.get_argument("older_than", None) and + not self.get_argument("newer_than", None)) + + query_older_than = self.get_argument("older_than", None) + query_newer_than = self.get_argument("newer_than", None) + + if query_older_than: + q["_id"] = {"$lt": ObjectId(query_older_than)} + elif query_newer_than: + q["_id"] = {"$gt": ObjectId(query_newer_than)} + query_sort = 1 + + outbox_data = list(db.find(q, limit=limit + 1).sort(sort_key, query_sort)) + outbox_len = len(outbox_data) + outbox_data = sorted( + outbox_data[:limit], key=lambda x: str(x[sort_key]), reverse=True + ) + if query_older_than: + newer_than = str(outbox_data[0]["_id"]) + if outbox_len == limit + 1: + older_than = str(outbox_data[-1]["_id"]) + elif query_newer_than: + older_than = str(outbox_data[-1]["_id"]) + if outbox_len == limit + 1: + newer_than = str(outbox_data[0]["_id"]) + elif first_page and outbox_len == limit + 1: + older_than = str(outbox_data[-1]["_id"]) + return outbox_data, older_than, newer_than + + +@app.context_processor +def context_processor(self): + q = { + "type": "Create", + "activity.object.type": "Note", + "activity.object.inReplyTo": None, + "meta.deleted": False, + } + notes_count = self.database.activities.find( + {"box": "outbox", "$or": [q, {"type": "Announce", "meta.undo": False}]} + ).count() + q = {"type": "Create", "activity.object.type": "Note", "meta.deleted": False} + with_replies_count = self.database.activities.find( + {"box": "outbox", "$or": [q, {"type": "Announce", "meta.undo": False}]} + ).count() + liked_count = self.database.activities.count( + { + "box": "outbox", + "meta.deleted": False, + "meta.undo": False, + "type": "Like", + } + ) + followers_q = { + "box": "inbox", + "type": "follow", + "meta.undo": False, + } + following_q = { + "box": "outbox", + "type": "follow", + "meta.undo": False, + } + return { + "microblogpub_version": VERSION, + "followers_count": self.database.activities.count(followers_q), + "following_count": self.database.activities.count(following_q), + "notes_count": notes_count, + "liked_count": liked_count, + "with_replies_count": with_replies_count, + "DOMAIN": "localhost:%s/test" % (self.port,), # TODO: update on each fetch, include full URL, /test + } + +@app.route("/test") +def route_test(self): + return self.render_template("test.html") + +### The filters: + +@app.filter +def html2plaintext(self, body, *args, **kwargs): + return html2text(body) + +def _to_list(item): + if not isinstance(item, list): + return list(item) + return item + +@app.filter +def has_type(self, doc, _type): + if _type in _to_list(doc["type"]): + return True + return False + +@app.filter +def get_actor(self, url): + retval = self.database.actors.find_one({"id": self.config["ID"]}) + if retval is not None: + return retval + +@app.filter +def get_url(self, u): + if isinstance(u, dict): + return u["href"] + elif isinstance(u, str): + return u + else: + return u + +@app.filter +def get_actor_icon_url(self, url, size): + return _get_file_url(url, size, Kind.ACTOR_ICON) + +@app.filter +def domain(self, url): + return urlparse(url).netloc + +@app.filter +def permalink_id(self, val): + return str(hash(val)) + +@app.filter +def is_from_outbox(self, t): + logging.warning("is_from_outbox(%s)" % (t,)) + return True + return t.startswith(ID) + +@app.filter +def format_timeago(self, val): + return "OK" + if val: + dt = parser.parse(val) + return timeago.format(dt, datetime.now(timezone.utc)) + return val + +# HTML/templates helper +ALLOWED_TAGS = [ + "a", + "abbr", + "acronym", + "b", + "br", + "blockquote", + "code", + "pre", + "em", + "i", + "li", + "ol", + "strong", + "ul", + "span", + "div", + "p", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", +] + +def clean_html(html): + return bleach.clean(html, tags=ALLOWED_TAGS) + +@app.filter +def clean(self, html): + return clean_html(html) + +@app.filter +def not_only_imgs(self, attachment): + for a in attachment: + if not _is_img(a["url"]): + return True + return False + +@app.filter +def is_img(self, filename): + return _is_img(filename) + +@app.filter +def get_attachment_url(self, url, size): + return _get_file_url(url, size, Kind.ATTACHMENT) + +@app.filter +def format_time(self, val): + return "OK" + if val: + dt = parser.parse(val) + return datetime.strftime(dt, "%B %d, %Y, %H:%M %p") + return val + +@app.filter +def quote_plus(self, t): + import urllib + return urllib.parse.quote_plus(t) + +if __name__ == "__main__": + manager.run() diff --git a/apps/blog/sass/base_theme.scss b/apps/blog/sass/base_theme.scss new file mode 100644 index 0000000..df2c4b3 --- /dev/null +++ b/apps/blog/sass/base_theme.scss @@ -0,0 +1,305 @@ +.note-container p:first-child { + margin-top: 0; +} +html, body { + height: 100%; +} + +@media only screen and (max-width: 480px) { + #menu-item-following { + display: none; + } +} +body { + background-color: $background-color; + color: $color; + display: flex; + flex-direction: column; +} +.base-container { + flex: 1 0 auto; +} +.footer { + flex-shrink: 0; +} +a, h1, h2, h3, h4, h5, h6 { + color: $color-title-link; +} +a { + text-decoration: none; +} +a:hover { + text-decoration: underline; +} +.gold { + color: $primary-color; +} +.pcolor { + color: $primary-color; +} +.lcolor { + color: $color-light; +} +.older-link, .newer-linker, .older-link:hover, .newer-link:hover { + text-decoration: none; + padding: 3px; +} +.newer-link { float: right } +.clear { clear: both; } +.remote-follow-button { + background: $color-menu-background; + color: $color-light; + text-decoration: none; + padding: 5px 8px; + margin-top: 5px; + border-radius: 2px; +} +.remote-follow-button:hover { + text-decoration: none; + background: $primary-color; + color: $background-color; +} +#admin-menu-wrapper { + padding: 10px; + margin:0 auto; + width: 100%; + background: $color-menu-background; + max-width: 720px; + +#admin-menu { + list-style-type: none; + display: inline; + padding: 10px; + color: $color-light; + border-radius-bottom-left: 2px; + border-radius-bottom-right: 2px; + .left { float: left; } + .right { float: right; } + li { + a { text-decoration: none; } + .admin-title { + text-transform: uppercase; + font-weight: bold; + } + padding-right:10px; + .selected, a:hover { + color: $primary-color; + } + } +} +} +#header { + margin-bottom: 70px; + + .title { + font-size: 1.2em; + padding-right: 15px; + color: $color-title-link; + } + .title:hover { + text-decoration: none; + } + .subtitle-username { + color: $color; + } + .menu { + clear: both; + padding: 0 0 10px 0; + ul { + display: inline; + list-style-type: none; + padding: 0; + li { + float: left; + margin-bottom: 10px; + margin-right: 10px; + } + } + a { + padding: 5px 10px; + small.badge { + background-color: $color-menu-background; + color: $color-light; + border-radius: 2px; + margin-left: 5px; + padding: 3px 5px 0px 5px; + font-weight: bold; + } + } + a.selected { + background: $primary-color; + color: $background-color; + border-radius:2px; + .badge { + color: $primary-color; + background: $background-color; + } + + } + a:hover { + background: $primary-color; + color: $background-color; + text-decoration: none; + border-radius: 2px; + .badge { + color: $primary-color; + background: $background-color; + } + } + } +} +#container { + width: 90%; + max-width: 720px; + margin: 30px auto; +} +#container #notes { + margin-top: 20px; +} +.actor-box { + display: block; + text-decoration: none; + margin-bottom: 40px; + + .actor-icon { + width: 120px; + border-radius:2px; + } + + h3 { margin: 0; } + + .actor-inline { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } +} +.actor-box-big { + display: block; + text-decoration: none; + + .actor-box-wrapper { + margin-bottom:40px; + + .actor-icon { + width:120px; + border-radius:2px; + } + + h3 { margin: 0; } + } +} +.note { + display: flex; + margin-bottom: 70px; + .l { + color: $color-note-link; + } + + .h-card { + flex: initial; + width: 50px; + } + + .u-photo { + width: 50px; + border-radius: 2px; + } + .note-wrapper { + flex: 1; + padding-left: 15px; + } + + .bottom-bar { margin-top:10px; } + + .img-attachment { + max-width:100%; + border-radius:2px; + } + + h3 { + font-size: 1.1em; + color: $color-light; + } + + strong { font-weight:600; } + + .note-container { + clear: right; + padding:10px 0; + } +} + +.bar-item-no-hover { + background: $color-menu-background; + padding: 5px; + color: $color-light; + margin-right:5px; + border-radius:2px; +} + +.bar-item { + background: $color-menu-background; + padding: 5px; + color: $color-light; + margin-right:5px; + border-radius:2px; +} +.bar-item:hover { + background: $primary-color; + color: $background-color; +} +button.bar-item { + border: 0 +} +form.action-form { + display: inline; +} +.perma { + font-size: 1.25em; +} +.bottom-bar .perma-item { + margin-right: 5px; +} +.bottom-bar a.bar-item:hover { + text-decoration: none; +} +.footer > div { + width: 90%; + max-width: 720px; + margin: 40px auto; +} +.footer a, .footer a:hover, .footer a:visited { + text-decoration: underline; + color: $color; +} +.summary { + color: $color-summary; + font-size: 1.1em; + margin-top: 10px; + margin-bottom: 30px; +} +.summary a, .summay a:hover { + color: $color-summary; + text-decoration: underline; +} +#followers, #following, #new { + margin-top: 50px; +} +#admin { + margin-top: 50px; +} +textarea, input { + background: $color-menu-background; + padding: 10px; + color: $color-light; + border: 0px; + border-radius: 2px; +} +input { + padding: 10px; +} +input[type=submit] { + color: $primary-color; + text-transform: uppercase; +} diff --git a/apps/blog/sass/dark.scss b/apps/blog/sass/dark.scss new file mode 100644 index 0000000..68a9f40 --- /dev/null +++ b/apps/blog/sass/dark.scss @@ -0,0 +1,8 @@ +$background-color: #060606; +$background-light: #222; +$color: #808080; +$color-title-link: #fefefe; +$color-summary: #ddd; +$color-light: #bbb; +$color-menu-background: #222; +$color-note-link: #666; diff --git a/apps/blog/sass/light.scss b/apps/blog/sass/light.scss new file mode 100644 index 0000000..9c4c251 --- /dev/null +++ b/apps/blog/sass/light.scss @@ -0,0 +1,9 @@ +$background-color: #eee; +$background-light: #ccc; +$color: #111; +$color-title-link: #333; +$color-light: #555; +$color-summary: #111; +$color-note-link: #333; +$color-menu-background: #ddd; +// $primary-color: #1d781d; diff --git a/apps/blog/sass/theme.scss b/apps/blog/sass/theme.scss new file mode 100644 index 0000000..6277839 --- /dev/null +++ b/apps/blog/sass/theme.scss @@ -0,0 +1 @@ +@import 'base_theme.scss' diff --git a/apps/blog/static/css/base_theme.css b/apps/blog/static/css/base_theme.css new file mode 100644 index 0000000..a2fe8c7 --- /dev/null +++ b/apps/blog/static/css/base_theme.css @@ -0,0 +1 @@ +.note-container p:first-child{margin-top:0}html,body{height:100%}body{background-color:#eee;color:#111;display:flex;flex-direction:column}.base-container{flex:1 0 auto}.footer{flex-shrink:0}a,h1,h2,h3,h4,h5,h6{color:#333}a{text-decoration:none}a:hover{text-decoration:underline}.gold{color:#1d781d}#header{margin-bottom:40px}#header .title{font-size:1.2em;padding-right:15px;color:#333}#header .title:hover{text-decoration:none}#header .subtitle-username{color:#111}#header .menu{padding:20px 0 10px 0}#header .menu ul{display:inline;list-style-type:none;padding:0}#header .menu ul li{float:left;padding-right:10px;margin-bottom:10px}#header .menu a{padding:2px 7px}#header .menu a.selected{background:#1d781d;color:#eee;border-radius:2px}#header .menu a:hover{background:#1d781d;color:#eee;text-decoration:none}#container{width:90%;max-width:720px;margin:40px auto}#container #notes{margin-top:20px}.actor-box{display:block;text-decoration:none;margin-bottom:40px}.actor-box .actor-icon{width:100%;max-width:120px;border-radius:2px}.actor-box h3{margin:0}.note{display:flex;margin-bottom:70px}.note .l{color:#333}.note .h-card{flex:initial;width:50px}.note .u-photo{width:50px;border-radius:2px}.note .note-wrapper{flex:1;padding-left:15px}.note .bottom-bar{margin-top:10px}.note .img-attachment{max-width:100%;border-radius:2px}.note h3{font-size:1.1em;color:#555}.note strong{font-weight:600}.note .note-container{clear:right;padding:10px 0}.bar-item{background:#ddd;padding:5px;color:#555;margin-right:5px;border-radius:2px}button.bar-item{border:0}form.action-form{display:inline}.bottom-bar .perma-item{margin-right:5px}.bottom-bar a.bar-item:hover{text-decoration:none}.footer>div{width:90%;max-width:720px;margin:40px auto}.footer a,.footer a:hover,.footer a:visited{text-decoration:underline;color:#111}.summary{color:#111;font-size:1.3em;margin-top:50px;margin-bottom:70px}.summary a,.summay a:hover{color:#111;text-decoration:underline}#followers,#following,#new{margin-top:50px}#admin{margin-top:50px}textarea,input{background:#ddd;padding:10px;color:#555;border:0px;border-radius:2px}input{padding:10px}input[type=submit]{color:#1d781d;text-transform:uppercase} diff --git a/apps/blog/static/media/.gitignore b/apps/blog/static/media/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/apps/blog/static/media/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/apps/blog/static/nopic.png b/apps/blog/static/nopic.png new file mode 100644 index 0000000..988d806 Binary files /dev/null and b/apps/blog/static/nopic.png differ diff --git a/apps/blog/templates/404.html b/apps/blog/templates/404.html new file mode 100644 index 0000000..aafdac3 --- /dev/null +++ b/apps/blog/templates/404.html @@ -0,0 +1,13 @@ +{% extends "layout.html" %} +{% import 'utils.html' as utils with context %} +{% block header %} +{% endblock %} +{% block content %} +404 Error: Not Found
+ ++
+ {% endif %} + + {% if item.meta.object %} + {{ utils.display_note(item.meta.object, ui=False) }} + {% endif %} + {% elif item | has_type('Create') %} + {{ utils.display_note(item.activity.object, meta=item.meta, no_color=True) }} + {% endif %} + + {% endfor %} + + {{ utils.display_pagination(older_than, newer_than) }} + +wants you to login
++ {% if item.meta.object %} + {{ utils.display_note(item.meta.object, ui=True) }} + {% endif %} + {% endif %} + + {% if item | has_type('Follow') %} +
+
+
+
{{ obj.summary | clean }}
{% endif %} +