Compare commits
No commits in common. "main" and "activity_pub" have entirely different histories.
main
...
activity_p
|
@ -1,45 +0,0 @@
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [created]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release-linux-amd64:
|
|
||||||
name: release linux/amd64
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version: '1.22'
|
|
||||||
- uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: '3.11'
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: go build -v ./...
|
|
||||||
|
|
||||||
- name: Test
|
|
||||||
run: go test -v ./...
|
|
||||||
|
|
||||||
- name: E2E Test
|
|
||||||
run: |
|
|
||||||
cd e2e_tests
|
|
||||||
docker compose -f docker-compose.ci.yml up -d
|
|
||||||
pip install -r requirements.txt
|
|
||||||
pytest
|
|
||||||
|
|
||||||
- name: Build Release
|
|
||||||
env:
|
|
||||||
CGO_ENABLED: 1
|
|
||||||
GOOS: linux
|
|
||||||
GOARCH: amd64
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
go build -o owl-linux-amd64 ./cmd/owl
|
|
||||||
gh release upload ${{github.event.release.tag_name}} owl-linux-amd64
|
|
|
@ -1,38 +0,0 @@
|
||||||
# This workflow will build a golang project
|
|
||||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
|
|
||||||
|
|
||||||
name: Go
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "main" ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ "main" ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version: '1.22'
|
|
||||||
- uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: '3.11'
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: go build -v ./...
|
|
||||||
|
|
||||||
- name: Test
|
|
||||||
run: go test -v ./...
|
|
||||||
|
|
||||||
- name: E2E Test
|
|
||||||
run: |
|
|
||||||
cd e2e_tests
|
|
||||||
docker compose -f docker-compose.ci.yml up -d
|
|
||||||
pip install -r requirements.txt
|
|
||||||
pytest
|
|
44
README.md
44
README.md
|
@ -2,9 +2,17 @@
|
||||||
|
|
||||||
# Owl Blogs
|
# Owl Blogs
|
||||||
|
|
||||||
Owl-blogs is a blogging software focused on simplicity with IndieWeb and Fediverse support.
|
A simple web server for blogs generated from Markdown files.
|
||||||
|
|
||||||
# Usage
|
**_This project is not yet stable. Expect frequent breaking changes! Only use this if you are willing to regularly adjust your project accordingly._**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```
|
||||||
|
CGO_ENABLED=1 go build -o owl ./cmd/owl
|
||||||
|
```
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
|
@ -27,35 +35,3 @@ To retrieve a list of all commands run:
|
||||||
```
|
```
|
||||||
owl -h
|
owl -h
|
||||||
```
|
```
|
||||||
|
|
||||||
# Development
|
|
||||||
|
|
||||||
## Build
|
|
||||||
|
|
||||||
```
|
|
||||||
CGO_ENABLED=1 go build -o owl ./cmd/owl
|
|
||||||
```
|
|
||||||
|
|
||||||
For development with live reload use `air` ([has to be install first](https://github.com/cosmtrek/air))
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
The project has two test suites; "unit tests" written in go and "end-to-end tests" written in python.
|
|
||||||
|
|
||||||
### Unit Tests
|
|
||||||
|
|
||||||
```
|
|
||||||
go test ./...
|
|
||||||
```
|
|
||||||
|
|
||||||
### End-to-End tests
|
|
||||||
|
|
||||||
- Start the docker compose setup in the `e2e_tests` directory.
|
|
||||||
- Install the python dependencies into a virtualenv
|
|
||||||
```
|
|
||||||
cd e2e_tests
|
|
||||||
python3 -m venv venv
|
|
||||||
. venv/bin/activate
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
- Run the e2e_tests with `pytest`
|
|
||||||
|
|
|
@ -498,41 +498,7 @@ func (svc *ActivityPubService) NotifyEntryCreated(entry model.Entry) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *ActivityPubService) NotifyEntryUpdated(entry model.Entry) {
|
func (svc *ActivityPubService) NotifyEntryUpdated(entry model.Entry) {
|
||||||
slog.Info("Processing Entry Create for ActivityPub")
|
|
||||||
followers, err := svc.AllFollowers()
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Cannot retrieve followers")
|
|
||||||
}
|
|
||||||
|
|
||||||
object, err := svc.entryToObject(entry)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Cannot convert object", "err", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
update := vocab.UpdateNew(object.ID, object)
|
|
||||||
update.Actor = object.AttributedTo
|
|
||||||
update.To = object.To
|
|
||||||
update.Published = object.Published
|
|
||||||
data, err := jsonld.WithContext(
|
|
||||||
jsonld.IRI(vocab.ActivityBaseURI),
|
|
||||||
jsonld.Context{
|
|
||||||
jsonld.ContextElement{
|
|
||||||
Term: "toot",
|
|
||||||
IRI: jsonld.IRI("http://joinmastodon.org/ns#"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
).Marshal(update)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("marshalling error", "err", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, follower := range followers {
|
|
||||||
actor, err := svc.GetActor(follower)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Unable to retrieve follower actor", "err", err)
|
|
||||||
}
|
|
||||||
svc.sendObject(actor, data)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *ActivityPubService) NotifyEntryDeleted(entry model.Entry) {
|
func (svc *ActivityPubService) NotifyEntryDeleted(entry model.Entry) {
|
||||||
|
@ -577,13 +543,6 @@ func (svc *ActivityPubService) entryToObject(entry model.Entry) (vocab.Object, e
|
||||||
if imageEntry, ok := entry.(*entrytypes.Image); ok {
|
if imageEntry, ok := entry.(*entrytypes.Image); ok {
|
||||||
return svc.imageToObject(imageEntry), nil
|
return svc.imageToObject(imageEntry), nil
|
||||||
}
|
}
|
||||||
if articleEntry, ok := entry.(*entrytypes.Article); ok {
|
|
||||||
return svc.articleToObject(articleEntry), nil
|
|
||||||
}
|
|
||||||
if recipeEntry, ok := entry.(*entrytypes.Recipe); ok {
|
|
||||||
return svc.recipeToObject(recipeEntry), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Warn("entry type not yet supported for activity pub")
|
slog.Warn("entry type not yet supported for activity pub")
|
||||||
return vocab.Object{}, errors.New("entry type not supported")
|
return vocab.Object{}, errors.New("entry type not supported")
|
||||||
}
|
}
|
||||||
|
@ -636,14 +595,11 @@ func (svc *ActivityPubService) imageToObject(imageEntry *entrytypes.Image) vocab
|
||||||
Type: vocab.DocumentType,
|
Type: vocab.DocumentType,
|
||||||
MediaType: vocab.MimeType(binaryFile.Mime()),
|
MediaType: vocab.MimeType(binaryFile.Mime()),
|
||||||
URL: vocab.ID(fullImageUrl),
|
URL: vocab.ID(fullImageUrl),
|
||||||
Name: vocab.NaturalLanguageValues{
|
|
||||||
{Value: vocab.Content(content)},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
image := vocab.Image{
|
image := vocab.Note{
|
||||||
ID: vocab.ID(imageEntry.FullUrl(siteCfg)),
|
ID: vocab.ID(imageEntry.FullUrl(siteCfg)),
|
||||||
Type: "Image",
|
Type: "Note",
|
||||||
To: vocab.ItemCollection{
|
To: vocab.ItemCollection{
|
||||||
vocab.PublicNS,
|
vocab.PublicNS,
|
||||||
vocab.IRI(svc.FollowersUrl()),
|
vocab.IRI(svc.FollowersUrl()),
|
||||||
|
@ -654,7 +610,7 @@ func (svc *ActivityPubService) imageToObject(imageEntry *entrytypes.Image) vocab
|
||||||
{Value: vocab.Content(imageEntry.Title())},
|
{Value: vocab.Content(imageEntry.Title())},
|
||||||
},
|
},
|
||||||
Content: vocab.NaturalLanguageValues{
|
Content: vocab.NaturalLanguageValues{
|
||||||
{Value: vocab.Content(imageEntry.Title() + "<br><br>" + string(content))},
|
{Value: vocab.Content(content)},
|
||||||
},
|
},
|
||||||
Attachment: attachments,
|
Attachment: attachments,
|
||||||
// Tag: tags,
|
// Tag: tags,
|
||||||
|
@ -662,51 +618,3 @@ func (svc *ActivityPubService) imageToObject(imageEntry *entrytypes.Image) vocab
|
||||||
return image
|
return image
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *ActivityPubService) articleToObject(articleEntry *entrytypes.Article) vocab.Object {
|
|
||||||
siteCfg, _ := svc.siteConfigServcie.GetSiteConfig()
|
|
||||||
content := articleEntry.Content()
|
|
||||||
|
|
||||||
image := vocab.Article{
|
|
||||||
ID: vocab.ID(articleEntry.FullUrl(siteCfg)),
|
|
||||||
Type: "Article",
|
|
||||||
To: vocab.ItemCollection{
|
|
||||||
vocab.PublicNS,
|
|
||||||
vocab.IRI(svc.FollowersUrl()),
|
|
||||||
},
|
|
||||||
Published: *articleEntry.PublishedAt(),
|
|
||||||
AttributedTo: vocab.ID(svc.ActorUrl()),
|
|
||||||
Name: vocab.NaturalLanguageValues{
|
|
||||||
{Value: vocab.Content(articleEntry.Title())},
|
|
||||||
},
|
|
||||||
Content: vocab.NaturalLanguageValues{
|
|
||||||
{Value: vocab.Content(string(content))},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return image
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *ActivityPubService) recipeToObject(recipeEntry *entrytypes.Recipe) vocab.Object {
|
|
||||||
siteCfg, _ := svc.siteConfigServcie.GetSiteConfig()
|
|
||||||
content := recipeEntry.Content()
|
|
||||||
|
|
||||||
image := vocab.Article{
|
|
||||||
ID: vocab.ID(recipeEntry.FullUrl(siteCfg)),
|
|
||||||
Type: "Article",
|
|
||||||
To: vocab.ItemCollection{
|
|
||||||
vocab.PublicNS,
|
|
||||||
vocab.IRI(svc.FollowersUrl()),
|
|
||||||
},
|
|
||||||
Published: *recipeEntry.PublishedAt(),
|
|
||||||
AttributedTo: vocab.ID(svc.ActorUrl()),
|
|
||||||
Name: vocab.NaturalLanguageValues{
|
|
||||||
{Value: vocab.Content(recipeEntry.Title())},
|
|
||||||
},
|
|
||||||
Content: vocab.NaturalLanguageValues{
|
|
||||||
{Value: vocab.Content(string(content))},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return image
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -48,13 +48,7 @@ func (s *EntryService) Create(entry model.Entry) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// only notify if the publishing date is set
|
s.Bus.NotifyCreated(entry)
|
||||||
// otherwise this is a draft.
|
|
||||||
// listeners might publish the entry to other services/platforms
|
|
||||||
// this should only happen for publshed content
|
|
||||||
if entry.PublishedAt() != nil && !entry.PublishedAt().IsZero() {
|
|
||||||
s.Bus.NotifyCreated(entry)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,13 +57,7 @@ func (s *EntryService) Update(entry model.Entry) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// only notify if the publishing date is set
|
s.Bus.NotifyUpdated(entry)
|
||||||
// otherwise this is a draft.
|
|
||||||
// listeners might publish the entry to other services/platforms
|
|
||||||
// this should only happen for publshed content
|
|
||||||
if entry.PublishedAt() != nil && !entry.PublishedAt().IsZero() {
|
|
||||||
s.Bus.NotifyUpdated(entry)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,9 +66,6 @@ func (s *EntryService) Delete(entry model.Entry) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// deletes should always be notfied
|
|
||||||
// a published entry might be converted to a draft before deletion
|
|
||||||
// omitting the deletion in this case would prevent deletion on other platforms
|
|
||||||
s.Bus.NotifyDeleted(entry)
|
s.Bus.NotifyDeleted(entry)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,9 +14,7 @@ func setupService() *app.EntryService {
|
||||||
register := app.NewEntryTypeRegistry()
|
register := app.NewEntryTypeRegistry()
|
||||||
register.Register(&test.MockEntry{})
|
register.Register(&test.MockEntry{})
|
||||||
repo := infra.NewEntryRepository(db, register)
|
repo := infra.NewEntryRepository(db, register)
|
||||||
cfgRepo := infra.NewConfigRepo(db)
|
service := app.NewEntryService(repo, nil, app.NewEventBus())
|
||||||
cfgService := app.NewSiteConfigService(cfgRepo)
|
|
||||||
service := app.NewEntryService(repo, cfgService, app.NewEventBus())
|
|
||||||
return service
|
return service
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ func (svc *SiteConfigService) defaultConfig() model.SiteConfig {
|
||||||
return model.SiteConfig{
|
return model.SiteConfig{
|
||||||
Title: "My Owl-Blog",
|
Title: "My Owl-Blog",
|
||||||
SubTitle: "A freshly created blog",
|
SubTitle: "A freshly created blog",
|
||||||
|
HeaderColor: "#efc48c",
|
||||||
PrimaryColor: "#d37f12",
|
PrimaryColor: "#d37f12",
|
||||||
AuthorName: "",
|
AuthorName: "",
|
||||||
Me: []model.MeLinks{},
|
Me: []model.MeLinks{},
|
||||||
|
|
|
@ -92,6 +92,7 @@ var importCmd = &cobra.Command{
|
||||||
}
|
}
|
||||||
v2Config.Title = v1Config.Title
|
v2Config.Title = v1Config.Title
|
||||||
v2Config.SubTitle = v1Config.SubTitle
|
v2Config.SubTitle = v1Config.SubTitle
|
||||||
|
v2Config.HeaderColor = v1Config.HeaderColor
|
||||||
v2Config.AuthorName = v1Config.AuthorName
|
v2Config.AuthorName = v1Config.AuthorName
|
||||||
v2Config.Me = mes
|
v2Config.Me = mes
|
||||||
v2Config.Lists = lists
|
v2Config.Lists = lists
|
||||||
|
|
|
@ -22,6 +22,7 @@ type MenuItem struct {
|
||||||
type SiteConfig struct {
|
type SiteConfig struct {
|
||||||
Title string
|
Title string
|
||||||
SubTitle string
|
SubTitle string
|
||||||
|
HeaderColor string
|
||||||
PrimaryColor string
|
PrimaryColor string
|
||||||
AuthorName string
|
AuthorName string
|
||||||
Me []MeLinks
|
Me []MeLinks
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
services:
|
|
||||||
web:
|
|
||||||
build:
|
|
||||||
context: ../
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
command: web
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
mock_masto:
|
|
||||||
build: mock_masto
|
|
||||||
ports:
|
|
||||||
- 8000:8000
|
|
|
@ -1,17 +1,11 @@
|
||||||
certifi==2024.2.2
|
certifi==2024.2.2
|
||||||
cffi==1.16.0
|
|
||||||
charset-normalizer==3.3.2
|
charset-normalizer==3.3.2
|
||||||
cryptography==42.0.7
|
|
||||||
exceptiongroup==1.2.1
|
exceptiongroup==1.2.1
|
||||||
http-message-signatures==0.5.0
|
|
||||||
http_sfv==0.9.9
|
|
||||||
idna==3.7
|
idna==3.7
|
||||||
iniconfig==2.0.0
|
iniconfig==2.0.0
|
||||||
packaging==24.0
|
packaging==24.0
|
||||||
pluggy==1.5.0
|
pluggy==1.5.0
|
||||||
pycparser==2.22
|
|
||||||
pytest==8.2.0
|
pytest==8.2.0
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
tomli==2.0.1
|
tomli==2.0.1
|
||||||
typing_extensions==4.11.0
|
|
||||||
urllib3==2.2.1
|
urllib3==2.2.1
|
||||||
|
|
|
@ -57,7 +57,6 @@ def test_following(client, inbox_url, followers_url, actor_url):
|
||||||
|
|
||||||
def test_unfollow(client, inbox_url, followers_url, actor_url):
|
def test_unfollow(client, inbox_url, followers_url, actor_url):
|
||||||
ensure_follow(client, inbox_url, actor_url)
|
ensure_follow(client, inbox_url, actor_url)
|
||||||
sleep(0.5)
|
|
||||||
with msg_inc(1):
|
with msg_inc(1):
|
||||||
req = sign(
|
req = sign(
|
||||||
"POST",
|
"POST",
|
||||||
|
|
|
@ -14,11 +14,6 @@
|
||||||
|
|
||||||
<link rel='stylesheet' href='/static/owl.css'>
|
<link rel='stylesheet' href='/static/owl.css'>
|
||||||
<link rel='stylesheet' href='/static/style.css'>
|
<link rel='stylesheet' href='/static/style.css'>
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--primary: {{.SiteConfig.PrimaryColor}};
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{{ .SiteConfig.HtmlHeadExtra }}
|
{{ .SiteConfig.HtmlHeadExtra }}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -72,10 +67,6 @@
|
||||||
<div>
|
<div>
|
||||||
{{ .SiteConfig.FooterExtra}}
|
{{ .SiteConfig.FooterExtra}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top:var(--s2);">
|
|
||||||
powered by <i><a href="https://github.com/H4kor/owl-blogs" target="_blank">owl-blogs</a></i>
|
|
||||||
</a>
|
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -14,6 +14,9 @@
|
||||||
<label for="SubTitle">SubTitle</label>
|
<label for="SubTitle">SubTitle</label>
|
||||||
<input type="text" name="SubTitle" id="SubTitle" value="{{.SubTitle}}"/>
|
<input type="text" name="SubTitle" id="SubTitle" value="{{.SubTitle}}"/>
|
||||||
|
|
||||||
|
<label for="HeaderColor">HeaderColor</label>
|
||||||
|
<input type="color" name="HeaderColor" id="HeaderColor" value="{{.HeaderColor}}"/>
|
||||||
|
|
||||||
<label for="PrimaryColor">PrimaryColor</label>
|
<label for="PrimaryColor">PrimaryColor</label>
|
||||||
<input type="color" name="PrimaryColor" id="PrimaryColor" value="{{.PrimaryColor}}"/>
|
<input type="color" name="PrimaryColor" id="PrimaryColor" value="{{.PrimaryColor}}"/>
|
||||||
|
|
||||||
|
|
|
@ -39,8 +39,7 @@ func NewWebApp(
|
||||||
apService *app.ActivityPubService,
|
apService *app.ActivityPubService,
|
||||||
) *WebApp {
|
) *WebApp {
|
||||||
fiberApp := fiber.New(fiber.Config{
|
fiberApp := fiber.New(fiber.Config{
|
||||||
BodyLimit: 50 * 1024 * 1024, // 50MB in bytes
|
BodyLimit: 50 * 1024 * 1024, // 50MB in bytes
|
||||||
DisableStartupMessage: true,
|
|
||||||
})
|
})
|
||||||
fiberApp.Use(middleware.NewUserMiddleware(authorService).Handle)
|
fiberApp.Use(middleware.NewUserMiddleware(authorService).Handle)
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ func (h *SiteConfigHandler) HandlePost(c *fiber.Ctx) error {
|
||||||
|
|
||||||
siteConfig.Title = c.FormValue("Title")
|
siteConfig.Title = c.FormValue("Title")
|
||||||
siteConfig.SubTitle = c.FormValue("SubTitle")
|
siteConfig.SubTitle = c.FormValue("SubTitle")
|
||||||
|
siteConfig.HeaderColor = c.FormValue("HeaderColor")
|
||||||
siteConfig.PrimaryColor = c.FormValue("PrimaryColor")
|
siteConfig.PrimaryColor = c.FormValue("PrimaryColor")
|
||||||
siteConfig.AuthorName = c.FormValue("AuthorName")
|
siteConfig.AuthorName = c.FormValue("AuthorName")
|
||||||
siteConfig.AvatarUrl = c.FormValue("AvatarUrl")
|
siteConfig.AvatarUrl = c.FormValue("AvatarUrl")
|
||||||
|
|
Loading…
Reference in New Issue