Compare commits
15 Commits
activity_p
...
main
Author | SHA1 | Date |
---|---|---|
Niko Abeler | da06541e11 | |
Niko Abeler | 7b047a609f | |
Niko Abeler | a5f24427a1 | |
Niko Abeler | ec13fffbe9 | |
Niko Abeler | 686cd72ec2 | |
Niko Abeler | 29e875d2e5 | |
Niko Abeler | f958fa36fd | |
Niko Abeler | 1b347bcdac | |
Niko Abeler | c8b759a834 | |
Niko Abeler | 943bc10eaf | |
Niko Abeler | 4543e448ed | |
Niko Abeler | e0d6f4f223 | |
Niko Abeler | 49a602f68b | |
Niko Abeler | 596ab0047e | |
Niko Abeler | 5c05f48be3 |
|
@ -0,0 +1,45 @@
|
|||
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
|
|
@ -0,0 +1,38 @@
|
|||
# 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,17 +2,9 @@
|
|||
|
||||
# Owl Blogs
|
||||
|
||||
A simple web server for blogs generated from Markdown files.
|
||||
Owl-blogs is a blogging software focused on simplicity with IndieWeb and Fediverse support.
|
||||
|
||||
**_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
|
||||
```
|
||||
# Usage
|
||||
|
||||
## Run
|
||||
|
||||
|
@ -35,3 +27,35 @@ To retrieve a list of all commands run:
|
|||
```
|
||||
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,7 +498,41 @@ func (svc *ActivityPubService) NotifyEntryCreated(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) {
|
||||
|
@ -543,6 +577,13 @@ func (svc *ActivityPubService) entryToObject(entry model.Entry) (vocab.Object, e
|
|||
if imageEntry, ok := entry.(*entrytypes.Image); ok {
|
||||
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")
|
||||
return vocab.Object{}, errors.New("entry type not supported")
|
||||
}
|
||||
|
@ -595,11 +636,14 @@ func (svc *ActivityPubService) imageToObject(imageEntry *entrytypes.Image) vocab
|
|||
Type: vocab.DocumentType,
|
||||
MediaType: vocab.MimeType(binaryFile.Mime()),
|
||||
URL: vocab.ID(fullImageUrl),
|
||||
Name: vocab.NaturalLanguageValues{
|
||||
{Value: vocab.Content(content)},
|
||||
},
|
||||
})
|
||||
|
||||
image := vocab.Note{
|
||||
image := vocab.Image{
|
||||
ID: vocab.ID(imageEntry.FullUrl(siteCfg)),
|
||||
Type: "Note",
|
||||
Type: "Image",
|
||||
To: vocab.ItemCollection{
|
||||
vocab.PublicNS,
|
||||
vocab.IRI(svc.FollowersUrl()),
|
||||
|
@ -610,7 +654,7 @@ func (svc *ActivityPubService) imageToObject(imageEntry *entrytypes.Image) vocab
|
|||
{Value: vocab.Content(imageEntry.Title())},
|
||||
},
|
||||
Content: vocab.NaturalLanguageValues{
|
||||
{Value: vocab.Content(content)},
|
||||
{Value: vocab.Content(imageEntry.Title() + "<br><br>" + string(content))},
|
||||
},
|
||||
Attachment: attachments,
|
||||
// Tag: tags,
|
||||
|
@ -618,3 +662,51 @@ func (svc *ActivityPubService) imageToObject(imageEntry *entrytypes.Image) vocab
|
|||
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,7 +48,13 @@ func (s *EntryService) Create(entry model.Entry) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Bus.NotifyCreated(entry)
|
||||
// only notify if the publishing date is set
|
||||
// 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
|
||||
}
|
||||
|
||||
|
@ -57,7 +63,13 @@ func (s *EntryService) Update(entry model.Entry) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Bus.NotifyUpdated(entry)
|
||||
// only notify if the publishing date is set
|
||||
// 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
|
||||
}
|
||||
|
||||
|
@ -66,6 +78,9 @@ func (s *EntryService) Delete(entry model.Entry) error {
|
|||
if err != nil {
|
||||
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)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -14,7 +14,9 @@ func setupService() *app.EntryService {
|
|||
register := app.NewEntryTypeRegistry()
|
||||
register.Register(&test.MockEntry{})
|
||||
repo := infra.NewEntryRepository(db, register)
|
||||
service := app.NewEntryService(repo, nil, app.NewEventBus())
|
||||
cfgRepo := infra.NewConfigRepo(db)
|
||||
cfgService := app.NewSiteConfigService(cfgRepo)
|
||||
service := app.NewEntryService(repo, cfgService, app.NewEventBus())
|
||||
return service
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,6 @@ func (svc *SiteConfigService) defaultConfig() model.SiteConfig {
|
|||
return model.SiteConfig{
|
||||
Title: "My Owl-Blog",
|
||||
SubTitle: "A freshly created blog",
|
||||
HeaderColor: "#efc48c",
|
||||
PrimaryColor: "#d37f12",
|
||||
AuthorName: "",
|
||||
Me: []model.MeLinks{},
|
||||
|
|
|
@ -92,7 +92,6 @@ var importCmd = &cobra.Command{
|
|||
}
|
||||
v2Config.Title = v1Config.Title
|
||||
v2Config.SubTitle = v1Config.SubTitle
|
||||
v2Config.HeaderColor = v1Config.HeaderColor
|
||||
v2Config.AuthorName = v1Config.AuthorName
|
||||
v2Config.Me = mes
|
||||
v2Config.Lists = lists
|
||||
|
|
|
@ -22,7 +22,6 @@ type MenuItem struct {
|
|||
type SiteConfig struct {
|
||||
Title string
|
||||
SubTitle string
|
||||
HeaderColor string
|
||||
PrimaryColor string
|
||||
AuthorName string
|
||||
Me []MeLinks
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
services:
|
||||
web:
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: Dockerfile
|
||||
command: web
|
||||
ports:
|
||||
- "3000:3000"
|
||||
mock_masto:
|
||||
build: mock_masto
|
||||
ports:
|
||||
- 8000:8000
|
|
@ -1,11 +1,17 @@
|
|||
certifi==2024.2.2
|
||||
cffi==1.16.0
|
||||
charset-normalizer==3.3.2
|
||||
cryptography==42.0.7
|
||||
exceptiongroup==1.2.1
|
||||
http-message-signatures==0.5.0
|
||||
http_sfv==0.9.9
|
||||
idna==3.7
|
||||
iniconfig==2.0.0
|
||||
packaging==24.0
|
||||
pluggy==1.5.0
|
||||
pycparser==2.22
|
||||
pytest==8.2.0
|
||||
requests==2.31.0
|
||||
tomli==2.0.1
|
||||
typing_extensions==4.11.0
|
||||
urllib3==2.2.1
|
||||
|
|
|
@ -57,6 +57,7 @@ def test_following(client, inbox_url, followers_url, actor_url):
|
|||
|
||||
def test_unfollow(client, inbox_url, followers_url, actor_url):
|
||||
ensure_follow(client, inbox_url, actor_url)
|
||||
sleep(0.5)
|
||||
with msg_inc(1):
|
||||
req = sign(
|
||||
"POST",
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
|
||||
<link rel='stylesheet' href='/static/owl.css'>
|
||||
<link rel='stylesheet' href='/static/style.css'>
|
||||
<style>
|
||||
:root {
|
||||
--primary: {{.SiteConfig.PrimaryColor}};
|
||||
}
|
||||
</style>
|
||||
{{ .SiteConfig.HtmlHeadExtra }}
|
||||
</head>
|
||||
<body>
|
||||
|
@ -67,6 +72,10 @@
|
|||
<div>
|
||||
{{ .SiteConfig.FooterExtra}}
|
||||
</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>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -14,9 +14,6 @@
|
|||
<label for="SubTitle">SubTitle</label>
|
||||
<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>
|
||||
<input type="color" name="PrimaryColor" id="PrimaryColor" value="{{.PrimaryColor}}"/>
|
||||
|
||||
|
|
|
@ -39,7 +39,8 @@ func NewWebApp(
|
|||
apService *app.ActivityPubService,
|
||||
) *WebApp {
|
||||
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)
|
||||
|
||||
|
|
|
@ -38,7 +38,6 @@ func (h *SiteConfigHandler) HandlePost(c *fiber.Ctx) error {
|
|||
|
||||
siteConfig.Title = c.FormValue("Title")
|
||||
siteConfig.SubTitle = c.FormValue("SubTitle")
|
||||
siteConfig.HeaderColor = c.FormValue("HeaderColor")
|
||||
siteConfig.PrimaryColor = c.FormValue("PrimaryColor")
|
||||
siteConfig.AuthorName = c.FormValue("AuthorName")
|
||||
siteConfig.AvatarUrl = c.FormValue("AvatarUrl")
|
||||
|
|
Loading…
Reference in New Issue