Compare commits

...

20 Commits

Author SHA1 Message Date
1e62814108 Add note and drop logging [ci skip] 2021-06-16 20:47:55 +02:00
bc7f51098e Fix bad typo
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-16 17:04:41 +02:00
6256260057 Ignore test files
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-16 17:02:46 +02:00
f677e27282 Attempt to log more and find correct paths 2021-06-16 17:02:34 +02:00
c1b25603a8 Ignore more stuff 2021-06-16 17:02:29 +02:00
f66c2e0396 More info from the script 2021-06-16 17:02:23 +02:00
b61342b576 Fix logging
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-16 16:14:07 +02:00
0aa5c1b625 Pass env vars, push does build now 2021-06-16 15:54:32 +02:00
5b4fe5fc54 Support shares from NC side
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-16 15:26:04 +02:00
f84c65fab8 Use patched pyocclient 2021-06-16 15:03:05 +02:00
d77cdb5243 Trying to describe the chaos further
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-16 13:32:08 +02:00
a436b6ff92 Update script to actually work 2021-06-16 12:59:33 +02:00
bc8dc62ca1 Add experimental external script
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-16 12:07:33 +02:00
a1eb570b67 Don't explode so much while testing
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-16 11:32:14 +02:00
8776b16c4a Fix deployment
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-16 11:28:39 +02:00
152610f394 Add badge
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-16 11:24:08 +02:00
40c5635620 Add drone config
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-16 11:23:05 +02:00
bf5c0c96e3 Drop dep 2021-06-16 11:22:42 +02:00
1aa3187212 Add entrypoint file 2021-06-16 11:21:29 +02:00
49597980cc Add deployment 2021-06-16 11:17:32 +02:00
12 changed files with 258 additions and 18 deletions

View File

@ -1,4 +1,5 @@
.envrc
.git .git
.mypy_cache
.venv .venv
__pycache__ __pycache__
.mypy_cache

19
.drone.yml Normal file
View File

@ -0,0 +1,19 @@
---
kind: pipeline
name: publish pipeline
steps:
- name: publish container
image: plugins/docker
settings:
username:
from_secret: docker_reg_username
password:
from_secret: docker_reg_passwd
repo: decentral1se/pubspace
auto_tag: true
trigger:
branch:
- main
event:
exclude:
- pull_request

14
.env.sample Normal file
View File

@ -0,0 +1,14 @@
# Local development
export MASTODON_ACCESS_TOKEN=foobar
export MASTODON_API_BASE_URL=social.lumbung.space
export APP_LOG_LEVEL=info
export NEXTCLOUD_API_BASE_URL=cloud.lumbung.space
export NEXTCLOUD_USER=decentral1se
export NEXTCLOUD_APP_PASSWORD=barfoo
# Deployment
export STACK_NAME=publish_lumbung_space
export DOMAIN=publish.lumbung.space
export ENTRYPOINT_CONF_VERSION=v1
export SECRET_MASTODON_ACCESS_TOKEN=v1
export SECRET_NEXTCLOUD_APP_PASSWORD=v1

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
*.pyc *.pyc
/__pycache__/ /__pycache__/
test.py

View File

@ -1,23 +1,57 @@
# pubspace # pubspace
> **WARNING**: this was an experimental prototype to understand if an
> "always-on" intermediary service could facilitate digital publishing
> practices in the lumbung.space. We have since moved to the idea of a
> slow and not real-time mode of publishing which will happen in another
> repository.
[![Build Status](https://drone.autonomic.zone/api/badges/ruangrupa/pubspace/status.svg?ref=refs/heads/main)](https://drone.autonomic.zone/ruangrupa/pubspace)
A service to facilitate collective digital publishing practices. A service to facilitate collective digital publishing practices.
## Mapping out the pubspace ## How does publishing happen?
- **Services**: ### cloud.lumbung.space
- [cloud.lumbung.space](https://cloud.lumbung.space/) ([Nextcloud](https://nextcloud.com/)) > **WARNING**: Only file sharing is implemented
- [tv.lumbung.space](https://tv.lumbung.space/) ([Peertube](https://joinpeertube.org/))
- [social.lumbung.space](https://social.lumbung.space/) ([Hometown](https://github.com/hometown-fork/hometown))
- [lumbung.space](https://lumbung.space/) ([Hugo](https://gohugo.io/))
- **Types of shares**: - When a file is tagged with the `publish` tag, a share link will be generated
- This share link will be shown on the `publish.lumbung.space` prototype page
- **cloud.lumbung.space**: links to file paths ### social.lumbung.space
- **tv.lumbung.space**: links to videos
- **social.lumbung.space**: links to posts
- **Publishing flows**: - When a toot uses the hashtag `#pubspace` (a comment on a thread will also "unroll" the entire thread)
- **cloud.lumbung.space**: Using [Nextcloud flows](https://nextcloud.com/blog/nextcloud-flow-makes-it-easy-to-automate-actions-and-workflows/), we can trigger an outgoing HTTP request towards `pubspace` - Only toots with `visibility: public` will be published. Local only posts and otherwise private posts are respected.
- **tv.lumbung.space**: Using [a federation client](https://mastodonpy.readthedocs.io/) we can watch for specific hashtags to trigger publishing - This toot link will be shown on the `publish.lumbung.space` prototype page
- **social.lumbung.space**: Using [a federation client](https://mastodonpy.readthedocs.io/), we can watch for specific hashtags to trigger publishing
### tv.lumbung.space
- When someone comments on a published video using the `#pubspace` hashtag
- This toot link will be shown on the `publish.lumbung.space` prototype page
## Supported services
- [cloud.lumbung.space](https://cloud.lumbung.space/) ([Nextcloud](https://nextcloud.com/))
- [tv.lumbung.space](https://tv.lumbung.space/) ([Peertube](https://joinpeertube.org/))
- [social.lumbung.space](https://social.lumbung.space/) ([Hometown](https://github.com/hometown-fork/hometown))
## Nextcloud flow script
See the [pubspace.sh](./pubspace.sh) script. This is manually copied over into
`/var/www/html/pubspace/pubspace.sh` for now. There is a workflow scripts rule
configured that when a file is tagged, the script is run. The script sends the filename
to this service and then we use the [pyocclient](https://github.com/owncloud/pyocclient)
to generate a share for that file. The Nextcloud crontab runs the script.
## Deployment
> Work In Progress
```
$ printf $YOURMASTODONACCESSTOKEN | docker secret create publish_lumbung_space_access_token_v1 -
$ printf $YOURNEXTCLOUDAPPPASSWORD | docker secret create publish_lumbung_space_app_password_v1 -
$ cp .env.sample .env # and update the values to match the environment
$ set -a && source .env && set +a
$ docker stack deploy -c compose.yml publish_lumbung_space
```

59
compose.yml Normal file
View File

@ -0,0 +1,59 @@
---
version: "3.8"
services:
app:
image: "decentral1se/pubspace:latest"
environment:
- APP_LOG_LEVEL
- MASTODON_ACCESS_TOKEN_FILE=/run/secrets/access_token
- MASTODON_API_BASE_URL
- NEXTCLOUD_API_BASE_URL
- NEXTCLOUD_APP_PASSWORD_FILE=/run/secrets/app_password
- NEXTCLOUD_USER
secrets:
- access_token
- app_password
networks:
- proxy
configs:
- source: entrypoint_sh
target: /usr/local/bin/entrypoint.sh
mode: 0555
entrypoint: /usr/local/bin/entrypoint.sh
healthcheck:
test: curl --fail 0.0.0.0:8000/healthz || exit 1
deploy:
update_config:
failure_action: rollback
order: start-first
labels:
- "traefik.enable=true"
- "traefik.http.services.pubspace.loadbalancer.server.port=8000"
- "traefik.http.routers.pubspace.rule=Host(`${DOMAIN}`)"
- "traefik.http.routers.pubspace.entrypoints=web-secure"
- "traefik.http.routers.pubspace.tls.certresolver=production"
command: |
uvicorn
--host 0.0.0.0
--forwarded-allow-ips="*"
--proxy-headers
pubspace:app
networks:
proxy:
external: true
configs:
entrypoint_sh:
name: ${STACK_NAME}_entrypoint_conf_${ENTRYPOINT_CONF_VERSION}
file: entrypoint.sh.tmpl
template_driver: golang
secrets:
access_token:
external: true
name: ${STACK_NAME}_access_token_${SECRET_MASTODON_ACCESS_TOKEN}
app_password:
external: true
name: ${STACK_NAME}_app_password_${SECRET_NEXTCLOUD_APP_PASSWORD}

30
entrypoint.sh.tmpl Normal file
View File

@ -0,0 +1,30 @@
#! /bin/bash
set -eu
file_env() {
local var="$1"
local fileVar="${var}_FILE"
local def="${2:-}"
if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
echo >&2 "error: both $var and $fileVar are set (but are exclusive)"
exit 1
fi
local val="$def"
if [ "${!var:-}" ]; then
val="${!var}"
elif [ "${!fileVar:-}" ]; then
val="$(< "${!fileVar}")"
fi
export "$var"="$val"
unset "$fileVar"
}
file_env "MASTODON_ACCESS_TOKEN"
file_env "NEXTCLOUD_APP_PASSWORD"
echo "Passing it back to the upstream ENTRYPOINT/CMD..."
exec "$@"

View File

@ -1,5 +1,5 @@
.DEFAULT: run .DEFAULT: run
.PHONY: run .PHONY: run build push
run: run:
@if [ ! -d ".venv" ]; then \ @if [ ! -d ".venv" ]; then \
@ -8,3 +8,9 @@ run:
.venv/bin/poetry install; \ .venv/bin/poetry install; \
fi fi
.venv/bin/poetry run uvicorn pubspace:app --reload .venv/bin/poetry run uvicorn pubspace:app --reload
build:
@docker build -t decentral1se/pubspace .
push: build
@docker push decentral1se/pubspace

22
poetry.lock generated
View File

@ -258,6 +258,25 @@ category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pyocclient"
version = "0.4"
description = ""
category = "main"
optional = false
python-versions = "*"
develop = false
[package.dependencies]
requests = ">=2.0.1"
six = "*"
[package.source]
type = "git"
url = "https://github.com/decentral1se/pyocclient.git"
reference = "patched-madcap-branch"
resolved_reference = "be63a32f520b887948f20ae57ca887d85555f720"
[[package]] [[package]]
name = "python-dateutil" name = "python-dateutil"
version = "2.8.1" version = "2.8.1"
@ -433,7 +452,7 @@ python-versions = ">=3.6.1"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "85c658e333aaf6d368f65e4fdaec48b721db82160dc31f5bdadfad7d3ca36da0" content-hash = "2f8f656acf6e4dca956d207427e94a3d7ab2054ec1109e52ee895e5fe833215d"
[metadata.files] [metadata.files]
appdirs = [ appdirs = [
@ -582,6 +601,7 @@ pyflakes = [
{file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"},
{file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
] ]
pyocclient = []
python-dateutil = [ python-dateutil = [
{file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"},
{file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"},

View File

@ -1,12 +1,18 @@
import logging import logging
from os import environ from os import environ
from os.path import basename
import owncloud
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from mastodon import Mastodon, StreamListener from mastodon import Mastodon, StreamListener
MASTODON_ACCESS_TOKEN = environ.get("MASTODON_ACCESS_TOKEN") MASTODON_ACCESS_TOKEN = environ.get("MASTODON_ACCESS_TOKEN")
MASTODON_API_BASE_URL = environ.get("MASTODON_API_BASE_URL") MASTODON_API_BASE_URL = environ.get("MASTODON_API_BASE_URL")
NEXTCLOUD_API_BASE_URL = environ.get("NEXTCLOUD_API_BASE_URL")
NEXTCLOUD_USER = environ.get("NEXTCLOUD_USER")
NEXTCLOUD_APP_PASSWORD = environ.get("NEXTCLOUD_APP_PASSWORD")
APP_LOG_LEVEL = environ.get("APP_LOG_LEVEL") APP_LOG_LEVEL = environ.get("APP_LOG_LEVEL")
if APP_LOG_LEVEL == "info": if APP_LOG_LEVEL == "info":
APP_LOG_LEVEL = logging.INFO APP_LOG_LEVEL = logging.INFO
@ -19,6 +25,7 @@ mastodon = Mastodon(
access_token=MASTODON_ACCESS_TOKEN, access_token=MASTODON_ACCESS_TOKEN,
api_base_url=f"https://{MASTODON_API_BASE_URL}", api_base_url=f"https://{MASTODON_API_BASE_URL}",
) )
nextcloud = owncloud.Client(f"https://{NEXTCLOUD_API_BASE_URL}")
log = logging.getLogger("uvicorn") log = logging.getLogger("uvicorn")
log.setLevel(APP_LOG_LEVEL) log.setLevel(APP_LOG_LEVEL)
@ -33,8 +40,42 @@ class PubspaceListener(StreamListener):
mastodon.stream_hashtag("pubspace", PubspaceListener(), run_async=True) mastodon.stream_hashtag("pubspace", PubspaceListener(), run_async=True)
nextcloud.login(NEXTCLOUD_USER, NEXTCLOUD_APP_PASSWORD)
def create_share(fpath):
fname = basname(fpath)
fpaths = nextcloud.list("/", depth="infinity")
matching = [f.path for f in fpaths if fname in f.path][0]
if not nextcloud.is_shared(matching):
info = nextcloud.share_file_with_link(matching)
return info.get_link()
return None
@app.get("/") @app.get("/")
async def home(request: Request): async def home(request: Request):
app.state.log.info(await request.json()) try:
payload = await request.json()
except Exception:
request.app.state.log.info("Unable to parse request, bailing out")
return {"detail": "unknown request"}
request.app.state.log.info(f"Received: {payload}")
try:
file = payload["rel_path"]
link = create_share(file)
except Exception:
request.app.state.log.info(f"Failed to share {file}")
return {"detail": "failed to create share"}
if link:
request.app.state.log.info(f"Shared {file} on {link}")
else:
request.app.state.log.info(f"{file} already shared or failure!")
@app.get("/healthz")
async def healthz(request: Request):
return {"detail": "ALL ENGINES FIRING"}

14
pubspace.sh Normal file
View File

@ -0,0 +1,14 @@
#!/bin/bash
set -e
ACTORS_USER_ID="$1"
OWNER_USER_ID="$2"
NEXTCLOUD_RELATIVE_PATH="$3"
LOCALLY_AVAILABLE_FILE="$4"
/usr/bin/curl \
-H "Content-Type: application/json" \
-X GET \
-d "{\"actor_id\":\"${ACTORS_USER_ID}\", \"owner_id\":\"${OWNER_USER_ID}\", \"rel_path\":\"${NEXTCLOUD_RELATIVE_PATH}\", \"local_path\":\"${LOCALLY_AVAILABLE_PATH}\"}" \
https://publish.lumbung.space

View File

@ -10,6 +10,7 @@ python = "^3.9"
fastapi = "^0.65.2" fastapi = "^0.65.2"
uvicorn = {extras = ["standard"], version = "^0.14.0"} uvicorn = {extras = ["standard"], version = "^0.14.0"}
"Mastodon.py" = "^1.5.1" "Mastodon.py" = "^1.5.1"
pyocclient = { git = "https://github.com/decentral1se/pyocclient.git", branch = "patched-madcap-branch" }
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
black = "^21.6b0" black = "^21.6b0"