Compare commits
20 Commits
8dab40ab4d
...
main
Author | SHA1 | Date | |
---|---|---|---|
1e62814108
|
|||
bc7f51098e
|
|||
6256260057
|
|||
f677e27282
|
|||
c1b25603a8
|
|||
f66c2e0396
|
|||
b61342b576
|
|||
0aa5c1b625
|
|||
5b4fe5fc54
|
|||
f84c65fab8
|
|||
d77cdb5243
|
|||
a436b6ff92
|
|||
bc8dc62ca1
|
|||
a1eb570b67
|
|||
8776b16c4a
|
|||
152610f394
|
|||
40c5635620
|
|||
bf5c0c96e3
|
|||
1aa3187212
|
|||
49597980cc
|
@ -1,4 +1,5 @@
|
|||||||
|
.envrc
|
||||||
.git
|
.git
|
||||||
|
.mypy_cache
|
||||||
.venv
|
.venv
|
||||||
__pycache__
|
__pycache__
|
||||||
.mypy_cache
|
|
||||||
|
19
.drone.yml
Normal file
19
.drone.yml
Normal 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
14
.env.sample
Normal 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
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
/__pycache__/
|
/__pycache__/
|
||||||
|
test.py
|
||||||
|
62
README.md
62
README.md
@ -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.
|
||||||
|
|
||||||
|
[](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
59
compose.yml
Normal 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
30
entrypoint.sh.tmpl
Normal 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 "$@"
|
8
makefile
8
makefile
@ -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
22
poetry.lock
generated
@ -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"},
|
||||||
|
43
pubspace.py
43
pubspace.py
@ -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
14
pubspace.sh
Normal 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
|
@ -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"
|
||||||
|
Reference in New Issue
Block a user