Compare commits

...

26 Commits

Author SHA1 Message Date
edf5a029ee
Problem: unread channels are at random positions
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Solution: move unread channels to the top
2022-04-30 14:10:29 +02:00
7a4f0ce8e1
Problem: no source in micropub item
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Solution: add source to micropub new item
2022-04-20 13:44:19 +02:00
1505943783
Problem: testing on promotion should not happen
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Solution: only test on push
2022-04-20 13:31:44 +02:00
150d29d180
Problem: deployment with testing is too slow
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Solution: simplify deployment
2022-04-20 13:26:45 +02:00
caaa069660
Problem: no sources in database
All checks were successful
continuous-integration/drone/push Build is passing
Solution: add sources in database and tests
2022-04-20 13:22:06 +02:00
974f460f84
Problem: no feeds are shown as changed
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Solution: use || instead of &&. As soon as one item was added, the feed
should be marked as changed.
2022-04-17 20:05:10 +02:00
6b60f5670a
Problem: errors when timeline does not exist
All checks were successful
continuous-integration/drone/push Build is passing
Solution: cleanup handling of timelines
2022-04-17 15:44:11 +02:00
4f8b9d30c8
Problem: channels are created automatically
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Solution: cleanup channels and don't create them automatically
2022-04-17 15:16:37 +02:00
296e2c03af
Problem: when logging UpdateFeed calls we only see channel Id
All checks were successful
continuous-integration/drone/push Build is passing
Solution: show channel name
2022-04-17 14:59:58 +02:00
a5105b0ddb
Problem: 0 unread items are not sent
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Solution: remove check for unread, it's not working
2022-04-17 00:23:05 +02:00
e9c69c8eac
Problem: error when paging without items
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Solution: check if items are available
2022-04-17 00:15:35 +02:00
7da53da8e1
Problem: Items call returns 10 Items, 20 is better
Solution: Items returns 20 items now
2022-04-17 00:14:16 +02:00
8adfb56274
Problem: item counts don't update
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Solution: re-enable updateChannelUnreadCount
2022-04-17 00:09:26 +02:00
eba40a4eee
Problem: before pagination shows first items
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Solution: use greater than to filter first item
2022-04-16 23:56:06 +02:00
13484d1834
Problem: no events is sent when a channel is created
Solution: fix bug when checking if channel was created
2022-04-16 23:55:10 +02:00
6bf8417451
Problem: After and Before are not working really well
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Solution: Filter after and before and only add both when more items are
available
2022-04-16 21:23:10 +02:00
75914514ec
Problem: there is no insight into queries and searches
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Solution: log searches
2022-04-16 20:49:29 +02:00
1606711d98
Problem: Prev Page is for moving forward
All checks were successful
continuous-integration/drone/push Build is passing
Solution: Use after in pagination
2022-04-16 20:43:55 +02:00
416a3733de
Problem: can't copy templates
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Solution: don't copy templates
2022-04-16 15:39:57 +02:00
a3dd194472 Merge pull request 'Histogram bucket feed polling' (#11) from histogram-buckets-feed-polling into master
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
Reviewed-on: #11
2022-04-16 13:28:03 +00:00
a2f04e4d6e
Problem: strings.Title is deprecated
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Solution: use golang.org/x/text/cases instead
2022-04-16 15:12:58 +02:00
179955dbc7
Problem: we use older Go version
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
Solution: upgrade Go version to 1.18.1
2022-04-16 15:05:22 +02:00
c9543e7a83
Problem: feeds are fetched every hour and haven't changed, or have changed often
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
Solution: implement histogram buckets for feed polling

The feeds are now implemented in tiers. The tier is the bucket the feed
is in. To calculate the minutes to wait for when to fetch the next feed,
you add 2**tier minutes to the current time.
The feeds to fetch are filter by this time.
2022-04-16 14:55:22 +02:00
dd1cf843e4
Problem: the project does not have a CHANGELOG.md
Some checks failed
continuous-integration/drone/push Build is failing
Solution: create a CHANGELOG.md file
2021-11-22 21:52:51 +01:00
ede2da8f8d
Problem: templates are not used
All checks were successful
continuous-integration/drone/push Build is passing
Solution: remove templates directory
2021-11-20 23:27:06 +01:00
90074d28d6
Problem: licenses in files are not regular
All checks were successful
continuous-integration/drone/push Build is passing
Solution: Paste license on top of all files. This does not change the
license. It was already licensed as GPLv3.
2021-11-20 22:26:39 +01:00
72 changed files with 1479 additions and 729 deletions

View File

@ -1,32 +1,74 @@
---
kind: pipeline
type: docker
name: build
name: build and test
workspace:
base: /go
path: src/p83.nl/go/ekster
trigger:
event:
- push
services:
- name: redis
image: redis:5
- name: database
image: postgres:14
environment:
POSTGRES_DB: ekster_testing
POSTGRES_USER: postgres
POSTGRES_PASSWORD: simple
POSTGRES_HOST_AUTH_METHOD: trust
steps:
- name: testing
image: golang:1.16-alpine
image: golang:1.18-alpine
environment:
CGO_ENABLED: 0
GOOS: linux
GOARCH: amd64
commands:
- go version
- apk --no-cache add git
- go get -d -t ./...
- go install honnef.co/go/tools/cmd/staticcheck@latest
- go build p83.nl/go/ekster/cmd/eksterd
- go build -buildvcs=false p83.nl/go/ekster/cmd/eksterd
- go vet ./...
- go test -v ./...
- staticcheck ./...
---
kind: pipeline
type: docker
name: move to production
workspace:
base: /go
path: src/p83.nl/go/ekster
trigger:
event:
- promote
target:
- production
steps:
- name: build
image: golang:1.18-alpine
environment:
CGO_ENABLED: 0
GOOS: linux
GOARCH: amd64
commands:
- go version
- apk --no-cache add git
- go get -d -t ./...
- go build -buildvcs=false p83.nl/go/ekster/cmd/eksterd
- name: publish-personal
image: plugins/docker
depends_on:
- testing
- build
settings:
repo: registry.stuifzandapp.com/microsub-server
registry: registry.stuifzandapp.com
@ -34,24 +76,6 @@ steps:
from_secret: docker_username
password:
from_secret: docker_password
when:
event:
- promote
target:
- production
# - name: publish-docker
# image: plugins/docker
# depends_on:
# - testing
# settings:
# repo: pstuifzand/ekster
# tags:
# - alpine
# username:
# from_secret: docker_official_username
# password:
# from_secret: docker_official_password
- name: deploy
image: appleboy/drone-ssh
@ -66,8 +90,3 @@ steps:
- cd /home/microsub/microsub
- docker-compose pull web
- docker-compose up -d
when:
event:
- promote
target:
- production

26
CHANGELOG.md Normal file
View File

@ -0,0 +1,26 @@
# Changelog
## [Unreleased]
## [1.0.0-rc.1] - 2021-11-20
### Added
- Postgresql support for channels, feeds, items and subscriptions.
- Support "source" in items and feeds
### Changed
- Default channel backend is postgresql
### Fixed
- All `staticcheck` problems are fixed.
- Dependency between memorybackend and hubbackend removed and simplified.
### Deprecated
- All Redis timeline types are deprecated and will be removed in a later version.
[Unreleased]: https://git.p83.nl/peter/ekster/compare/1.0.0-rc.1...master
[1.0.0-rc.1]: https://git.p83.nl/peter/ekster/src/tag/1.0.0-rc.1

View File

@ -3,5 +3,4 @@ RUN apk --no-cache add ca-certificates
WORKDIR /opt/micropub
EXPOSE 80
COPY ./eksterd /app/
COPY ./templates /app/templates
ENTRYPOINT ["/app/eksterd"]

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// Ek is a microsub client.
package main

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (

View File

@ -0,0 +1,123 @@
/*
* Ekster is a microsub server
* Copyright (c) 2022 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"database/sql"
"log"
"net/http/httptest"
"os"
"testing"
"github.com/gomodule/redigo/redis"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type DatabaseSuite struct {
suite.Suite
URL string
Database *sql.DB
RedisURL string
Redis redis.Conn
}
func (s *DatabaseSuite) SetupSuite() {
db, err := sql.Open("postgres", s.URL)
if err != nil {
log.Fatal(err)
}
s.Database = db
conn, err := redis.Dial("tcp", s.RedisURL)
if err != nil {
log.Fatal(err)
}
s.Redis = conn
_, err = s.Redis.Do("SELECT", "1")
if err != nil {
log.Fatal(err)
}
}
func (s *DatabaseSuite) TearDownSuite() {
err := s.Database.Close()
if err != nil {
log.Fatal(err)
}
err = s.Redis.Close()
if err != nil {
log.Fatal(err)
}
}
type databaseSuite struct {
DatabaseSuite
}
func (d *databaseSuite) TestGetChannelFromAuthorization() {
_, err := d.Database.Exec(`truncate "sources", "channels", "feeds", "subscriptions","items"`)
assert.NoError(d.T(), err, "truncate sources, channels, feeds")
row := d.Database.QueryRow(`INSERT INTO "channels" (uid, name, created_at, updated_at) VALUES ('abcdef', 'Channel', now(), now()) RETURNING "id"`)
var id int
err = row.Scan(&id)
assert.NoError(d.T(), err, "insert channel")
_, err = d.Database.Exec(`INSERT INTO "sources" (channel_id, auth_code, created_at, updated_at) VALUES ($1, '1234', now(), now())`, id)
assert.NoError(d.T(), err, "insert sources")
// source_id found
r := httptest.NewRequest("POST", "/micropub?source_id=1234", nil)
_, c, err := getChannelFromAuthorization(r, d.Redis, d.Database)
assert.NoError(d.T(), err, "channel from source_id")
assert.Equal(d.T(), "abcdef", c, "channel uid found")
// source_id not found
r = httptest.NewRequest("POST", "/micropub?source_id=1111", nil)
_, c, err = getChannelFromAuthorization(r, d.Redis, d.Database)
assert.Error(d.T(), err, "channel from authorization header")
assert.Equal(d.T(), "", c, "channel uid found")
}
func TestDatabaseSuite(t *testing.T) {
if testing.Short() {
t.Skip("Skip test for database")
}
databaseURL := os.Getenv("DATABASE_TEST_URL")
if databaseURL == "" {
databaseURL = "host=database user=postgres password=simple dbname=ekster_testing sslmode=disable"
}
databaseSuite := &databaseSuite{
DatabaseSuite{
URL: databaseURL,
RedisURL: "redis:6379",
},
}
databaseURL = "postgres://postgres@database/ekster_testing?sslmode=disable&user=postgres&password=simple"
err := runMigrations(databaseURL)
if err != nil {
log.Fatal(err)
}
suite.Run(t, databaseSuite)
}

View File

@ -1 +1,19 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
DROP TABLE "channels";

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
CREATE TABLE IF NOT EXISTS "channels" (
"id" int primary key generated always as identity,
"uid" varchar(255) unique,

View File

@ -1 +1,19 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
DROP TABLE "feeds";

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
CREATE TABLE "feeds" (
"id" int primary key generated always as identity,
"channel_id" int references "channels" (id) on update cascade on delete cascade,

View File

@ -1 +1,19 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
DELETE FROM "channels" WHERE "uid" IN ('home', 'notifications');

View File

@ -1 +1,19 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
INSERT INTO "channels" ("uid", "name") VALUES ('home', 'Home'), ('notifications', 'Notifications');

View File

@ -1 +1,19 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
DROP TABLE "items";

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
CREATE TABLE IF NOT EXISTS "items" (
"id" int primary key generated always as identity,
"channel_id" int references "channels" on delete cascade,

View File

@ -1 +1,19 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
ALTER TABLE "items" DROP COLUMN "feed_id";

View File

@ -1 +1,19 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
ALTER TABLE "items" ADD COLUMN "feed_id" INT REFERENCES "feeds" ON DELETE CASCADE;

View File

@ -1 +1,19 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
DROP TABLE "subscriptions";

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
CREATE TABLE "subscriptions" (
"id" int primary key generated always as identity,
"topic" varchar(1024) not null references "feeds" ("url") on update cascade on delete cascade,

View File

@ -0,0 +1,4 @@
alter table "feeds"
drop column "tier",
drop column "unmodified",
drop column "next_fetch_at";

View File

@ -0,0 +1,4 @@
alter table "feeds"
add column "tier" int default 0,
add column "unmodified" int default 0,
add column "next_fetch_at" timestamptz;

View File

@ -0,0 +1 @@
DROP TABLE "sources";

View File

@ -0,0 +1,20 @@
BEGIN;
CREATE OR REPLACE FUNCTION update_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ language 'plpgsql';
COMMIT;
CREATE TABLE "sources" (
"id" int primary key generated always as identity,
"channel_id" int not null,
"auth_code" varchar(64) not null,
"created_at" timestamp DEFAULT current_timestamp,
"updated_at" timestamp DEFAULT current_timestamp
);
CREATE TRIGGER sources_update_timestamp BEFORE INSERT OR UPDATE ON "sources"
FOR EACH ROW EXECUTE PROCEDURE update_timestamp();

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
@ -117,15 +135,14 @@ func (h *hubIncomingBackend) UpdateFeed(processor ContentProcessor, subscription
log.Println("UpdateFeed", subscriptionID)
db := h.database
var (
topic string
channel string
feedID string
)
// Process all channels that contains this feed
rows, err := db.Query(
`select topic, c.uid, f.id from subscriptions s inner join feeds f on f.url = s.topic inner join channels c on c.id = f.channel_id where s.id = $1`,
rows, err := db.Query(`
select topic, c.uid, f.id, c.name
from subscriptions s
inner join feeds f on f.url = s.topic
inner join channels c on c.id = f.channel_id
where s.id = $1
`,
subscriptionID,
)
if err != nil {
@ -133,16 +150,18 @@ func (h *hubIncomingBackend) UpdateFeed(processor ContentProcessor, subscription
}
for rows.Next() {
err = rows.Scan(&topic, &channel, &feedID)
var topic, channel, feedID, channelName string
err = rows.Scan(&topic, &channel, &feedID, &channelName)
if err != nil {
log.Println(err)
continue
}
log.Printf("Updating feed %s %q in %q\n", feedID, topic, channel)
err = processor.ProcessContent(channel, feedID, topic, contentType, body)
log.Printf("Updating feed %s %q in %q (%s)\n", feedID, topic, channelName, channel)
_, err = processor.ProcessContent(channel, feedID, topic, contentType, body)
if err != nil {
log.Printf("could not process content for channel %s: %s", channel, err)
log.Printf("could not process content for channel %s: %s", channelName, err)
}
}
@ -205,22 +224,24 @@ func (h *hubIncomingBackend) Subscribe(feed *Feed) error {
}
func (h *hubIncomingBackend) run() error {
ticker := time.NewTicker(10 * time.Minute)
ticker := time.NewTicker(1 * time.Minute)
quit := make(chan struct{})
go func() {
for {
select {
case <-ticker.C:
log.Println("Getting feeds for WebSub")
log.Println("Getting feeds for WebSub started")
varWebsub.Add("runs", 1)
feeds, err := h.Feeds()
if err != nil {
log.Println("Feeds failed:", err)
log.Println("Getting feeds for WebSub completed")
continue
}
log.Printf("Found %d feeds", len(feeds))
for _, feed := range feeds {
log.Printf("Looking at %s\n", feed.URL)
if feed.ResubscribeAt != nil && time.Now().After(*feed.ResubscribeAt) {
@ -236,6 +257,8 @@ func (h *hubIncomingBackend) run() error {
}
}
}
log.Println("Getting feeds for WebSub completed")
case <-quit:
ticker.Stop()
return

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (

View File

@ -1,14 +1,20 @@
// Copyright (C) 2018 Peter Stuifzand
//
// This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
// License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
// later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with this program. If not,
// see <http://www.gnu.org/licenses/>.
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
Eksterd is a microsub server that is extendable.
@ -161,8 +167,8 @@ func main() {
// }
// TODO(peter): automatically gather this information from login or otherwise
err := runMigrations()
databaseURL := "postgres://postgres@database/ekster?sslmode=disable&user=postgres&password=simple"
err := runMigrations(databaseURL)
if err != nil {
log.Fatalf("Error with migrations: %s", err)
}
@ -199,12 +205,12 @@ func (l Log) Verbose() bool {
return false
}
func runMigrations() error {
func runMigrations(databaseURL string) error {
d, err := iofs.New(migrations, "db/migrations")
if err != nil {
return err
}
m, err := migrate.NewWithSourceInstance("iofs", d, "postgres://postgres@database/ekster?sslmode=disable&user=postgres&password=simple")
m, err := migrate.NewWithSourceInstance("iofs", d, databaseURL)
if err != nil {
return err
}

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
@ -9,14 +27,17 @@ import (
"io"
"io/ioutil"
"log"
"math"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/lib/pq"
"github.com/pkg/errors"
"p83.nl/go/ekster/pkg/auth"
"p83.nl/go/ekster/pkg/fetch"
"p83.nl/go/ekster/pkg/microsub"
@ -84,6 +105,15 @@ type newItemMessage struct {
Channel string `json:"channel"`
}
type feed struct {
UID string // channel
ID int
URL string
Tier int
Unmodified int
NextFetchAt time.Time
}
func (b *memoryBackend) AuthTokenAccepted(header string, r *auth.TokenResponse) (bool, error) {
conn := b.pool.Get()
defer func() {
@ -126,6 +156,9 @@ GROUP BY c.id;
}})
}
util.StablePartition(channels, 0, len(channels), func(i int) bool {
return channels[i].Unread.HasUnread()
})
return channels, nil
}
@ -157,7 +190,7 @@ func (b *memoryBackend) ChannelsCreate(name string) (microsub.Channel, error) {
for {
varMicrosub.Add("ChannelsCreate.RandStringBytes", 1)
channel.UID = util.RandStringBytes(24)
result, err := b.database.Exec(`insert into "channels" ("uid", "name", "created_at") values($1, $2, DEFAULT)`, channel.UID, channel.Name)
result, err := b.database.Exec(`insert into "channels" ("uid", "name", "created_at") values ($1, $2, DEFAULT)`, channel.UID, channel.Name)
if err != nil {
log.Println("channels insert", err)
if !shouldRetryWithNewUID(err, try) {
@ -166,7 +199,7 @@ func (b *memoryBackend) ChannelsCreate(name string) (microsub.Channel, error) {
try++
continue
}
if n, err := result.RowsAffected(); err != nil {
if n, err := result.RowsAffected(); err == nil {
if n > 0 {
b.broker.Notifier <- sse.Message{Event: "new channel", Object: channelMessage{1, channel}}
}
@ -201,15 +234,22 @@ func (b *memoryBackend) ChannelsDelete(uid string) error {
b.broker.Notifier <- sse.Message{Event: "delete channel", Object: channelDeletedMessage{1, uid}}
return nil
}
type feed struct {
UID string // channel
ID int
URL string
func (b *memoryBackend) updateFeed(feed feed) error {
_, err := b.database.Exec(`
UPDATE "feeds"
SET "tier" = $2, "unmodified" = $3, "next_fetch_at" = $4
WHERE "id" = $1
`, feed.ID, feed.Tier, feed.Unmodified, feed.NextFetchAt)
return err
}
func (b *memoryBackend) getFeeds() ([]feed, error) {
rows, err := b.database.Query(`SELECT "f"."id", "f"."url", "c"."uid" FROM "feeds" AS "f" INNER JOIN public.channels c on c.id = f.channel_id`)
rows, err := b.database.Query(`
SELECT "f"."id", "f"."url", "c"."uid", "f"."tier","f"."unmodified","f"."next_fetch_at"
FROM "feeds" AS "f"
INNER JOIN public.channels c ON c.id = f.channel_id
WHERE next_fetch_at IS NULL OR next_fetch_at < now()
`)
if err != nil {
return nil, err
}
@ -218,29 +258,49 @@ func (b *memoryBackend) getFeeds() ([]feed, error) {
for rows.Next() {
var feedID int
var feedURL, UID string
var tier, unmodified int
var nextFetchAt sql.NullTime
err = rows.Scan(&feedID, &feedURL, &UID)
err = rows.Scan(&feedID, &feedURL, &UID, &tier, &unmodified, &nextFetchAt)
if err != nil {
log.Printf("while scanning feeds: %s", err)
continue
}
feeds = append(feeds, feed{UID, feedID, feedURL})
var fetchTime time.Time
if nextFetchAt.Valid {
fetchTime = nextFetchAt.Time
} else {
fetchTime = time.Now()
}
feeds = append(
feeds,
feed{
UID: UID,
ID: feedID,
URL: feedURL,
Tier: tier,
Unmodified: unmodified,
NextFetchAt: fetchTime,
},
)
}
return feeds, nil
}
func (b *memoryBackend) run() {
b.ticker = time.NewTicker(10 * time.Minute)
b.ticker = time.NewTicker(1 * time.Minute)
b.quit = make(chan struct{})
go func() {
b.RefreshFeeds()
for {
select {
case <-b.ticker.C:
b.RefreshFeeds()
case <-b.quit:
b.ticker.Stop()
return
@ -250,43 +310,89 @@ func (b *memoryBackend) run() {
}
func (b *memoryBackend) RefreshFeeds() {
log.Println("Feed update process started")
defer log.Println("Feed update process completed")
feeds, err := b.getFeeds()
if err != nil {
return
}
count := 0
log.Printf("Found %d feeds", len(feeds))
count := 0
for _, feed := range feeds {
feedURL := feed.URL
feedID := feed.ID
uid := feed.UID
log.Println("Processing", feedURL)
resp, err := b.Fetch3(uid, feedURL)
log.Println("Processing", feed.URL)
err := b.refreshFeed(feed)
if err != nil {
log.Printf("Error while Fetch3 of %s: %v\n", feedURL, err)
b.addNotification("Error while fetching feed", feedURL, err)
count++
b.addNotification("Error while fetching feed", feed, err)
continue
}
err = b.ProcessContent(uid, fmt.Sprintf("%d", feedID), feedURL, resp.Header.Get("Content-Type"), resp.Body)
if err != nil {
log.Printf("Error while processing content for %s: %v\n", feedURL, err)
b.addNotification("Error while processing feed", feedURL, err)
count++
continue
}
_ = resp.Body.Close()
count++
}
if count > 0 {
_ = b.updateChannelUnreadCount("notifications")
}
log.Printf("Processed %d feeds", count)
}
func (b *memoryBackend) addNotification(name string, feedURL string, err error) {
err = b.channelAddItem("notifications", microsub.Item{
func (b *memoryBackend) refreshFeed(feed feed) error {
resp, err := b.Fetch3(feed.UID, feed.URL)
if err != nil {
return fmt.Errorf("while Fetch3 of %s: %w", feed.URL, err)
}
defer resp.Body.Close()
changed, err := b.ProcessContent(feed.UID, fmt.Sprintf("%d", feed.ID), feed.URL, resp.Header.Get("Content-Type"), resp.Body)
if err != nil {
return fmt.Errorf("in ProcessContent of %s: %w", feed.URL, err)
}
if changed {
feed.Tier--
} else {
feed.Unmodified++
}
if feed.Unmodified >= 2 {
feed.Tier++
feed.Unmodified = 0
}
if feed.Tier > 10 {
feed.Tier = 10
}
if feed.Tier < 0 {
feed.Tier = 0
}
minutes := time.Duration(math.Ceil(math.Exp2(float64(feed.Tier))))
feed.NextFetchAt = time.Now().Add(minutes * time.Minute)
log.Printf("Next Fetch in %d minutes at %v", minutes, feed.NextFetchAt.Format(time.RFC3339))
err = b.updateFeed(feed)
if err != nil {
log.Printf("Error: while updating feed %v: %v", feed, err)
// don't return error, because it becomes a notification
return nil
}
return nil
}
func (b *memoryBackend) addNotification(name string, feed feed, err error) {
_, err = b.channelAddItem("notifications", microsub.Item{
Type: "entry",
Source: &microsub.Source{
ID: strconv.Itoa(feed.ID),
URL: feed.URL,
Name: feed.URL,
},
Name: name,
Content: &microsub.Content{
Text: fmt.Sprintf("ERROR: while updating feed: %s", err),
@ -307,9 +413,12 @@ func (b *memoryBackend) TimelineGet(before, after, channel string) (microsub.Tim
return microsub.Timeline{Items: []microsub.Item{}}, err
}
timelineBackend := b.getTimeline(channel)
timelineBackend, err := b.getTimeline(channel)
if err != nil {
return microsub.Timeline{}, err
}
_ = b.updateChannelUnreadCount(channel)
// _ = b.updateChannelUnreadCount(channel)
return timelineBackend.Items(before, after)
}
@ -336,7 +445,7 @@ func (b *memoryBackend) FollowGetList(uid string) ([]microsub.Feed, error) {
}
func (b *memoryBackend) FollowURL(uid string, url string) (microsub.Feed, error) {
feed := microsub.Feed{Type: "feed", URL: url}
subFeed := microsub.Feed{Type: "feed", URL: url}
var channelID int
err := b.database.QueryRow(`SELECT "id" FROM "channels" WHERE "uid" = $1`, uid).Scan(&channelID)
@ -349,28 +458,36 @@ func (b *memoryBackend) FollowURL(uid string, url string) (microsub.Feed, error)
var feedID int
err = b.database.QueryRow(
`INSERT INTO "feeds" ("channel_id", "url") VALUES ($1, $2) RETURNING "id"`,
`INSERT INTO "feeds" ("channel_id", "url", "tier", "unmodified", "next_fetch_at") VALUES ($1, $2, 1, 0, now()) RETURNING "id"`,
channelID,
feed.URL,
subFeed.URL,
).Scan(&feedID)
if err != nil {
return feed, err
return subFeed, err
}
resp, err := b.Fetch3(uid, feed.URL)
var newFeed = feed{
ID: feedID,
UID: uid,
URL: url,
Tier: 1,
Unmodified: 0,
NextFetchAt: time.Now(),
}
resp, err := b.Fetch3(uid, subFeed.URL)
if err != nil {
log.Println(err)
b.addNotification("Error while fetching feed", feed.URL, err)
b.addNotification("Error while fetching feed", newFeed, err)
_ = b.updateChannelUnreadCount("notifications")
return feed, err
return subFeed, err
}
defer resp.Body.Close()
_ = b.ProcessContent(uid, fmt.Sprintf("%d", feedID), feed.URL, resp.Header.Get("Content-Type"), resp.Body)
_, _ = b.ProcessContent(uid, fmt.Sprintf("%d", feedID), subFeed.URL, resp.Header.Get("Content-Type"), resp.Body)
_, _ = b.hubBackend.CreateFeed(url)
return feed, nil
return subFeed, nil
}
func (b *memoryBackend) UnfollowURL(uid string, url string) error {
@ -507,15 +624,16 @@ func (b *memoryBackend) PreviewURL(previewURL string) (microsub.Timeline, error)
}
func (b *memoryBackend) MarkRead(channel string, uids []string) error {
tl := b.getTimeline(channel)
err := tl.MarkRead(uids)
tl, err := b.getTimeline(channel)
if err != nil {
return err
}
err = b.updateChannelUnreadCount(channel)
if err != nil {
if err = tl.MarkRead(uids); err != nil {
return err
}
if err = b.updateChannelUnreadCount(channel); err != nil {
return err
}
@ -566,31 +684,35 @@ func ProcessSourcedItems(fetcher fetch.Fetcher, fetchURL, contentType string, bo
// ContentProcessor processes content for a channel and feed
type ContentProcessor interface {
ProcessContent(channel, feedID, fetchURL, contentType string, body io.Reader) error
ProcessContent(channel, feedID, fetchURL, contentType string, body io.Reader) (bool, error)
}
func (b *memoryBackend) ProcessContent(channel, feedID, fetchURL, contentType string, body io.Reader) error {
// ProcessContent processes content of a feed, returns if the feed has changed or not
func (b *memoryBackend) ProcessContent(channel, feedID, fetchURL, contentType string, body io.Reader) (bool, error) {
cachingFetch := WithCaching(b.pool, fetch.FetcherFunc(Fetch2))
items, err := ProcessSourcedItems(cachingFetch, fetchURL, contentType, body)
if err != nil {
return err
return false, err
}
changed := false
for _, item := range items {
item.Source.ID = feedID
err = b.channelAddItemWithMatcher(channel, item)
added, err := b.channelAddItemWithMatcher(channel, item)
if err != nil {
log.Printf("ERROR: (feedID=%s) %s\n", feedID, err)
}
changed = changed || added
}
err = b.updateChannelUnreadCount(channel)
if err != nil {
return err
return changed, err
}
return nil
return changed, nil
}
// Fetch3 fills stuff
@ -599,17 +721,12 @@ func (b *memoryBackend) Fetch3(channel, fetchURL string) (*http.Response, error)
return Fetch2(fetchURL)
}
func (b *memoryBackend) channelAddItemWithMatcher(channel string, item microsub.Item) error {
func (b *memoryBackend) channelAddItemWithMatcher(channel string, item microsub.Item) (bool, error) {
// an item is posted
// check for all channels as channel
// if regex matches item
// - add item to channel
err := addToSearch(item, channel)
if err != nil {
return fmt.Errorf("addToSearch in channelAddItemWithMatcher: %v", err)
}
var updatedChannels []string
b.lock.RLock()
@ -622,23 +739,23 @@ func (b *memoryBackend) channelAddItemWithMatcher(channel string, item microsub.
switch v {
case "repost":
if len(item.RepostOf) > 0 {
return nil
return false, nil
}
case "like":
if len(item.LikeOf) > 0 {
return nil
return false, nil
}
case "bookmark":
if len(item.BookmarkOf) > 0 {
return nil
return false, nil
}
case "reply":
if len(item.InReplyTo) > 0 {
return nil
return false, nil
}
case "checkin":
if item.Checkin != nil {
return nil
return false, nil
}
}
}
@ -647,16 +764,24 @@ func (b *memoryBackend) channelAddItemWithMatcher(channel string, item microsub.
re, err := regexp.Compile(setting.IncludeRegex)
if err != nil {
log.Printf("error in regexp: %q, %s\n", setting.IncludeRegex, err)
return nil
return false, nil
}
if matchItem(item, re) {
log.Printf("Included %#v\n", item)
err := b.channelAddItem(channelKey, item)
added, err := b.channelAddItem(channelKey, item)
if err != nil {
continue
}
updatedChannels = append(updatedChannels, channelKey)
err = addToSearch(item, channel)
if err != nil {
return added, fmt.Errorf("addToSearch in channelAddItemWithMatcher: %v", err)
}
if added {
updatedChannels = append(updatedChannels, channelKey)
}
}
}
}
@ -679,15 +804,26 @@ func (b *memoryBackend) channelAddItemWithMatcher(channel string, item microsub.
excludeRegex, err := regexp.Compile(setting.ExcludeRegex)
if err != nil {
log.Printf("error in regexp: %q\n", excludeRegex)
return nil
return false, nil
}
if matchItem(item, excludeRegex) {
log.Printf("Excluded %#v\n", item)
return nil
return false, nil
}
}
return b.channelAddItem(channel, item)
added, err := b.channelAddItem(channel, item)
if err != nil {
return added, err
}
err = addToSearch(item, channel)
if err != nil {
return added, fmt.Errorf("addToSearch in channelAddItemWithMatcher: %v", err)
}
return added, nil
}
func matchItem(item microsub.Item, re *regexp.Regexp) bool {
@ -716,11 +852,15 @@ func matchItemText(item microsub.Item, re *regexp.Regexp) bool {
return re.MatchString(item.Name)
}
func (b *memoryBackend) channelAddItem(channel string, item microsub.Item) error {
timelineBackend := b.getTimeline(channel)
func (b *memoryBackend) channelAddItem(channel string, item microsub.Item) (bool, error) {
timelineBackend, err := b.getTimeline(channel)
if err != nil {
return false, err
}
added, err := timelineBackend.AddItem(item)
if err != nil {
return err
return added, err
}
// Sent message to Server-Sent-Events
@ -728,23 +868,33 @@ func (b *memoryBackend) channelAddItem(channel string, item microsub.Item) error
b.broker.Notifier <- sse.Message{Event: "new item", Object: newItemMessage{item, channel}}
}
return err
return added, err
}
// ErrNotUpdated is used when the unread count is not updated
var ErrNotUpdated = errors.New("timeline unread count not updated")
// ErrNotFound is used when the timeline is not found
var ErrNotFound = errors.New("timeline not found")
func (b *memoryBackend) updateChannelUnreadCount(channel string) error {
// tl := b.getTimeline(channel)
// unread, err := tl.Count()
// if err != nil {
// return err
// }
//
// currentCount := c.Unread.UnreadCount
// c.Unread = microsub.Unread{Type: microsub.UnreadCount, UnreadCount: unread}
//
// // Sent message to Server-Sent-Events
// if currentCount != unread {
// b.broker.Notifier <- sse.Message{Event: "new item in channel", Object: c}
// }
tl, err := b.getTimeline(channel)
if err != nil {
return err
}
unread, err := tl.Count()
if err != nil {
return ErrNotUpdated
}
var c = microsub.Channel{
UID: channel,
Unread: microsub.Unread{Type: microsub.UnreadCount, UnreadCount: unread},
}
// Sent message to Server-Sent-Events
b.broker.Notifier <- sse.Message{Event: "new item in channel", Object: c}
return nil
}
@ -824,15 +974,12 @@ func Fetch2(fetchURL string) (*http.Response, error) {
return resp, err
}
func (b *memoryBackend) getTimeline(channel string) timeline.Backend {
func (b *memoryBackend) getTimeline(channel string) (timeline.Backend, error) {
// Set a default timeline type if not set
timelineType := "postgres-stream"
// if setting, ok := b.Settings[channel]; ok && setting.ChannelType != "" {
// timelineType = setting.ChannelType
// }
tl := timeline.Create(channel, timelineType, b.pool, b.database)
if tl == nil {
log.Printf("no timeline found with name %q and type %q", channel, timelineType)
return tl, fmt.Errorf("timeline id %q: %w", channel, ErrNotFound)
}
return tl
return tl, nil
}

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
// func Test_memoryBackend_ChannelsCreate(t *testing.T) {

View File

@ -1,7 +1,26 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"crypto/sha1"
"database/sql"
"encoding/json"
"fmt"
"log"
@ -47,7 +66,7 @@ func (h *micropubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
var channel string
channel, err = getChannelFromAuthorization(r, conn)
sourceID, channel, err := getChannelFromAuthorization(r, conn, h.Backend.database)
if err != nil {
log.Println(err)
http.Error(w, "unauthorized", http.StatusUnauthorized)
@ -81,7 +100,12 @@ func (h *micropubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
item.ID = newID
err = h.Backend.channelAddItemWithMatcher(channel, *item)
item.Source = &microsub.Source{
ID: fmt.Sprintf("micropub:%d", sourceID),
Name: fmt.Sprintf("Source %d", sourceID),
}
_, err = h.Backend.channelAddItemWithMatcher(channel, *item)
if err != nil {
log.Printf("could not add item to channel %s: %v", channel, err)
}
@ -147,16 +171,23 @@ func parseIncomingItem(r *http.Request) (*microsub.Item, error) {
return nil, fmt.Errorf("content-type %q is not supported", contentType)
}
func getChannelFromAuthorization(r *http.Request, conn redis.Conn) (string, error) {
func getChannelFromAuthorization(r *http.Request, conn redis.Conn, database *sql.DB) (int, string, error) {
// backward compatible
sourceID := r.URL.Query().Get("source_id")
if sourceID != "" {
channel, err := redis.String(conn.Do("HGET", "sources", sourceID))
if err != nil {
return "", errors.Wrapf(err, "could not get channel for sourceID: %s", sourceID)
}
row := database.QueryRow(`
SELECT s.id as source_id, c.uid
FROM "sources" AS "s"
INNER JOIN "channels" AS "c" ON s.channel_id = c.id
WHERE "auth_code" = $1
`, sourceID)
return channel, nil
var channel string
var sourceID int
if err := row.Scan(&sourceID, &channel); err == sql.ErrNoRows {
return 0, "", errors.New("channel not found")
}
return sourceID, channel, nil
}
// full micropub with indieauth
@ -165,11 +196,11 @@ func getChannelFromAuthorization(r *http.Request, conn redis.Conn) (string, erro
token := authHeader[7:]
channel, err := redis.String(conn.Do("HGET", "token:"+token, "channel"))
if err != nil {
return "", errors.Wrap(err, "could not get channel for token")
return 0, "", errors.Wrap(err, "could not get channel for token")
}
return channel, nil
return 0, channel, nil
}
return "", fmt.Errorf("could not get channel from authorization")
return 0, "", fmt.Errorf("could not get channel from authorization")
}

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (

View File

@ -1,4 +1,3 @@
{{- /*gotype: p83.nl/go/ekster/cmd/eksterd.authPage*/ -}}
<!DOCTYPE html>
<html>
<head>

View File

@ -1,4 +1,3 @@
{{- /*gotype: p83.nl/go/ekster/cmd/eksterd.settingsPage*/ -}}
<!DOCTYPE html>
<html>
<head>

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (

1
go.mod
View File

@ -13,5 +13,6 @@ require (
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.7.0
golang.org/x/net v0.0.0-20211013171255-e13a2654a71e
golang.org/x/text v0.3.7
willnorris.com/go/microformats v1.1.0
)

1
go.sum
View File

@ -1234,6 +1234,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

View File

@ -1,6 +1,24 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package auth
// Auther
// Auther checks of the token in the head is accepted and fills TokenResponse
type Auther interface {
AuthTokenAccepted(header string, r *TokenResponse) bool
}

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package client
import (

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// Package fetch provides an API for fetching information about urls.
package fetch

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package fetch
import (

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package fetch
import "net/http"

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package indieauth
import (

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package jf2
import (

View File

@ -1,21 +1,22 @@
// Package jf2 converts microformats to JF2
/*
ekster - microsub server
Copyright (C) 2018 Peter Stuifzand
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// Package jf2 converts microformats to JF2
package jf2
import (

View File

@ -1,20 +1,20 @@
/*
ekster - microsub server
Copyright (C) 2018 Peter Stuifzand
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package jf2_test
import (

View File

@ -1,18 +1,22 @@
// Package jsonfeed contains the types and a parse function for JSON feeds.
// Copyright (C) 2018 Peter Stuifzand
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// Package jsonfeed parses feeds in the jsonfeed format
package jsonfeed
import (

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package jsonfeed
import (

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package microsub
import (

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// Package microsub describes the protocol methods of the Microsub protocol
package microsub

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package microsub
import (

View File

@ -5,8 +5,10 @@ import (
"encoding/xml"
"fmt"
"sort"
"strings"
"time"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
func parseRSS1(data []byte) (*Feed, error) {
@ -29,6 +31,8 @@ func parseRSS1(data []byte) (*Feed, error) {
out.Description = channel.Description
out.Link = channel.Link
out.Image = channel.Image.Image()
titleCaser := cases.Title(language.English)
if channel.MinsToLive != 0 {
sort.Ints(channel.SkipHours)
next := time.Now().Add(time.Duration(channel.MinsToLive) * time.Minute)
@ -41,7 +45,7 @@ func parseRSS1(data []byte) (*Feed, error) {
for trying {
trying = false
for _, day := range channel.SkipDays {
if strings.Title(day) == next.Weekday().String() {
if titleCaser.String(day) == next.Weekday().String() {
next.Add(time.Duration(24-next.Hour()) * time.Hour)
trying = true
break

View File

@ -5,8 +5,10 @@ import (
"encoding/xml"
"fmt"
"sort"
"strings"
"time"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
func parseRSS2(data []byte) (*Feed, error) {
@ -38,6 +40,7 @@ func parseRSS2(data []byte) (*Feed, error) {
out.Image = channel.Image.Image()
if channel.MinsToLive != 0 {
titleCaser := cases.Title(language.English)
sort.Ints(channel.SkipHours)
next := time.Now().Add(time.Duration(channel.MinsToLive) * time.Minute)
for _, hour := range channel.SkipHours {
@ -49,7 +52,7 @@ func parseRSS2(data []byte) (*Feed, error) {
for trying {
trying = false
for _, day := range channel.SkipDays {
if strings.Title(day) == next.Weekday().String() {
if titleCaser.String(day) == next.Weekday().String() {
next.Add(time.Duration(24-next.Hour()) * time.Hour)
trying = true
break

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
Package server contains the microsub server itself. It implements http.Handler.
It follows the spec at https://indieweb.org/Microsub-spec.
@ -221,6 +239,7 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
})
return
}
log.Printf("Searching for %s in %s (%d results)", query, channel, len(items))
respondJSON(w, map[string]interface{}{
"query": query,
"items": items,

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package server
import (

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package server
import (

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package sse
import (

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package timeline
import "p83.nl/go/ekster/pkg/microsub"

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package timeline
import (
@ -35,52 +53,11 @@ func (p *postgresStream) Init() error {
if err != nil {
return fmt.Errorf("database ping failed: %w", err)
}
//
// _, err = conn.ExecContext(ctx, `
// CREATE TABLE IF NOT EXISTS "channels" (
// "id" int primary key generated always as identity,
// "name" varchar(255) unique,
// "created_at" timestamp DEFAULT current_timestamp
// );
// `)
// if err != nil {
// return fmt.Errorf("create channels table failed: %w", err)
// }
//
// _, err = conn.ExecContext(ctx, `
// CREATE TABLE IF NOT EXISTS "items" (
// "id" int primary key generated always as identity,
// "channel_id" int references "channels" on delete cascade,
// "uid" varchar(512) not null unique,
// "is_read" int default 0,
// "data" jsonb,
// "created_at" timestamp DEFAULT current_timestamp,
// "updated_at" timestamp,
// "published_at" timestamp
// );
// `)
// if err != nil {
// return fmt.Errorf("create items table failed: %w", err)
// }
//
// _, err = conn.ExecContext(ctx, `ALTER TABLE "items" ALTER COLUMN "data" TYPE jsonb, ALTER COLUMN "uid" TYPE varchar(1024)`)
// if err != nil {
// return fmt.Errorf("alter items table failed: %w", err)
// }
_, err = conn.ExecContext(ctx, `INSERT INTO "channels" ("uid", "name", "created_at") VALUES ($1, $1, DEFAULT)
ON CONFLICT DO NOTHING`, p.channel)
if err != nil {
return fmt.Errorf("create channel failed: %w", err)
}
row := conn.QueryRowContext(ctx, `SELECT "id" FROM "channels" WHERE "uid" = $1`, p.channel)
if row == nil {
return fmt.Errorf("fetch channel failed: %w", err)
}
err = row.Scan(&p.channelID)
if err != nil {
return fmt.Errorf("fetch channel failed while scanning: %w", err)
if err == sql.ErrNoRows {
return fmt.Errorf("channel %s not found: %w", p.channel, err)
}
return nil
@ -109,16 +86,16 @@ WHERE "channel_id" = $1
log.Println(err)
} else {
args = append(args, b)
qb.WriteString(` AND "published_at" < $2`)
qb.WriteString(` AND "published_at" > $2`)
}
} else if after != "" {
b, err := time.Parse(time.RFC3339, after)
if err == nil {
args = append(args, b)
qb.WriteString(` AND "published_at" > $2`)
qb.WriteString(` AND "published_at" < $2`)
}
}
qb.WriteString(` ORDER BY "published_at" DESC LIMIT 10`)
qb.WriteString(` ORDER BY "published_at" DESC LIMIT 20`)
rows, err := conn.QueryContext(context.Background(), qb.String(), args...)
if err != nil {
@ -162,9 +139,12 @@ WHERE "channel_id" = $1
return tl, err
}
// TODO: should only be set of there are more items available
tl.Paging.Before = last
// tl.Paging.After = last
if len(tl.Items) > 0 && hasMoreBefore(conn, tl.Items[0].Published) {
tl.Paging.Before = tl.Items[0].Published
}
if hasMoreAfter(conn, last) {
tl.Paging.After = last
}
if tl.Items == nil {
tl.Items = []microsub.Item{}
@ -173,6 +153,24 @@ WHERE "channel_id" = $1
return tl, nil
}
func hasMoreBefore(conn *sql.Conn, before string) bool {
row := conn.QueryRowContext(context.Background(), `SELECT COUNT(*) FROM "items" WHERE "published_at" > $1`, before)
var count int
if err := row.Scan(&count); err == sql.ErrNoRows {
return false
}
return count > 0
}
func hasMoreAfter(conn *sql.Conn, after string) bool {
row := conn.QueryRowContext(context.Background(), `SELECT COUNT(*) FROM "items" WHERE "published_at" < $1`, after)
var count int
if err := row.Scan(&count); err == sql.ErrNoRows {
return false
}
return count > 0
}
// Count
func (p *postgresStream) Count() (int, error) {
ctx := context.Background()
@ -181,16 +179,12 @@ func (p *postgresStream) Count() (int, error) {
return -1, err
}
defer conn.Close()
var count int
row := conn.QueryRowContext(context.Background(), `SELECT COUNT(*) FROM items WHERE channel_id = $1 AND "is_read" = 0`, p.channelID)
if row == nil {
err = row.Scan(&count)
if err != nil && err == sql.ErrNoRows {
return 0, nil
}
var count int
err = row.Scan(&count)
if err != nil {
return -1, err
}
return count, nil
}

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package timeline
import (

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package timeline
import (

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// Package timeline contains different types of timeline backends.
//
// "sorted-set" uses Redis sorted sets as a backend

View File

@ -1,7 +1,26 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package util
import "reflect"
// Rotate rotates values of an array, between index f and l with midpoint k
func Rotate(a interface{}, f, k, l int) int {
swapper := reflect.Swapper(a)
if f == k {
@ -38,6 +57,7 @@ func Rotate(a interface{}, f, k, l int) int {
return ret
}
// StablePartition partitions elements of the array between indices f and l according to predicate p
func StablePartition(a interface{}, f, l int, p func(i int) bool) int {
n := l - f
@ -48,7 +68,7 @@ func StablePartition(a interface{}, f, l int, p func(i int) bool) int {
if n == 1 {
t := f
if p(f) {
t += 1
t++
}
return t
}

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package util
import (
@ -6,6 +24,7 @@ import (
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
// RandStringBytes generates a random string of n characters
func RandStringBytes(n int) string {
b := make([]byte, n)
for i := range b {

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package websub
import (

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package websub
import (

View File

@ -1,3 +1,21 @@
/*
* Ekster is a microsub server
* Copyright (c) 2021 The Ekster authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package websub
import (

View File

@ -1,100 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ekster</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css">
</head>
<body>
<section class="section">
<div class="container">
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/">
Ekster
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="menu">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
{{ if .Session.LoggedIn }}
<div id="menu" class="navbar-menu">
<a class="navbar-item" href="/settings">
Settings
</a>
<a class="navbar-item" href="/logs">
Logs
</a>
<a class="navbar-item" href="{{ .Session.Me }}">
Profile
</a>
</div>
{{ end }}
</nav>
<h1 class="title">Ekster - Microsub server</h1>
<div class="box">
<form action="/auth/approve" method="post">
<input type="hidden" name="state" value="{{ .State }}" />
<div class="field">
<label class="label">Client ID</label>
<div class="control">
<p>{{ .ClientID }}</p>
</div>
<div class="control">
<p>{{ .App.Name }}</p>
</div>
<div class="control">
<p><img src="{{ .App.IconURL }}" /></p>
</div>
</div>
<div class="field">
<label class="label">RedirectURI</label>
<div class="control">
<p>{{ .RedirectURI }}</p>
</div>
</div>
<div class="field">
<label class="label">Scope</label>
<div class="control">
<p>{{ .Scope }}</p>
</div>
</div>
<div class="field">
<label class="label">Select a channel</label>
<div class="control">
<div class="select">
<select name="channel">
{{ range .Channels }}
<option value="{{ .UID }}">{{ .Name }}</option>
{{ end }}
</select>
</div>
</div>
</div>
<div class="field">
<div class="control">
<button type="submit" name="accept" value="approve" class="button is-primary">
Approve
</button>
</div>
</div>
</form>
</div>
</div>
</section>
</body>
</html>

View File

@ -1,49 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{template "title" .}}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css">
</head>
<body>
<section class="section">
<div class="container">
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/">
Ekster
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="menu">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
{{ if .Session.LoggedIn }}
<div id="menu" class="navbar-menu">
<a class="navbar-item" href="/settings">
Settings
</a>
<a class="navbar-item" href="/logs">
Logs
</a>
<a class="navbar-item" href="{{ .Session.Me }}">
Profile
</a>
</div>
{{ end }}
</nav>
<h1 class="title">Ekster - Microsub server</h1>
{{template "content" .}}
</div>
</section>
</body>
</html>
{{define "header"}}{{end}}
{{define "content"}}{{end}}
{{define "footer"}}{{end}}

View File

@ -1,122 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ekster</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css">
</head>
<body>
<section class="section">
<div class="container">
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/">
Ekster
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="menu">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
{{ if .Session.LoggedIn }}
<div id="menu" class="navbar-menu">
<a class="navbar-item" href="/settings">
Settings
</a>
<a class="navbar-item" href="/logs">
Logs
</a>
<a class="navbar-item" href="{{ .Session.Me }}">
Profile
</a>
</div>
{{ end }}
</nav>
<h1 class="title">Ekster - Microsub server</h1>
{{ $channel := .CurrentChannel }}
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li><a href="/settings">Settings</a></li>
<li class="is-active"><a href="/setttings/channel?uid={{ .CurrentChannel }}">{{ $channel.Name }}</a></li>
</ul>
</nav>
<h2 class="subtitle is-2">{{ $channel.Name }}</h2>
<div class="columns">
<div class="column">
<h3 class="title is-4">Settings</h3>
<form action="/settings/channel" method="post">
<input type="hidden" name="uid" value="{{ .CurrentChannel.UID }}" />
<div class="field">
<label class="label" for="exclude_regex">Blocking Regex</label>
<div class="control">
<input type="text" class="input" id="exclude_regex" name="exclude_regex" value="{{ .CurrentSetting.ExcludeRegex }}" placeholder="enter regex to block" />
</div>
</div>
<div class="field">
<label class="label" for="include_regex">Tracking Regex</label>
<div class="control">
<input type="text" class="input" id="include_regex" name="include_regex" value="{{ .CurrentSetting.IncludeRegex }}" placeholder="enter regex to track items" />
</div>
</div>
<div class="field">
<label class="label" for="type">Channel Type</label>
<div class="control">
<div class="select">
<select name="type" id="type">
<option value="null" {{if eq (.CurrentSetting.ChannelType) "null" }}selected{{end}}>Null</option>
<option value="sorted-set" {{if eq (.CurrentSetting.ChannelType) "sorted-set" }}selected{{end}}>Sorted Set</option>
<option value="stream" {{if eq (.CurrentSetting.ChannelType) "stream" }}selected{{end}}>Streams</option>
<option value="postgres-stream" {{if eq (.CurrentSetting.ChannelType) "postgres-stream" }}selected{{end}}>Postgres Stream</option>
</select>
</div>
</div>
</div>
<div class="field">
<label for="exclude_type" class="label">Exclude Types</label>
<div class="control">
<div class="select is-multiple">
<select name="exclude_type" id="exclude_type" multiple>
{{ range $key, $excluded := $.ExcludedTypes }}
<option value="{{ $key }}" {{ if $excluded }}selected="selected"{{ end }}>{{ index $.ExcludedTypeNames $key }}</option>
{{ end }}
</select>
</div>
</div>
</div>
<div class="field">
<div class="control">
<button type="submit" class="button is-primary">Save</button>
</div>
</div>
</form>
</div>
<div class="column">
<h3 class="title is-4">Feeds</h3>
<div class="channel">
{{ range .Feeds }}
<div class="feed box">
<div class="name">
<a href="{{ .URL }}">{{ .URL }}</a>
</div>
</div>
{{ else }}
<div class="no-channels">No feeds</div>
{{ end }}
</div>
</div>
</div>
</div>
</section>
</body>
</html>

View File

@ -1,71 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ekster</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css">
<link rel="micropub" href="{{ .Baseurl }}/micropub" />
<link rel="authorization_endpoint" href="{{ .Baseurl }}/auth" />
<link rel="token_endpoint" href="{{ .Baseurl }}/auth/token" />
</head>
<body>
<section class="section">
<div class="container">
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/">
Ekster
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="menu">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
{{ if .Session.LoggedIn }}
<div id="menu" class="navbar-menu">
<a class="navbar-item" href="/settings">
Settings
</a>
<a class="navbar-item" href="/logs">
Logs
</a>
<a class="navbar-item" href="{{ .Session.Me }}">
Profile
</a>
</div>
{{ end }}
</nav>
<h1 class="title">Ekster - Microsub server</h1>
{{ if .Session.LoggedIn }}
<h2 class="title">Logout</h2>
<form action="/session/logout" method="post">
<button type="submit" class="button is-info">Logout</button>
</form>
{{ else }}
<h2 class="title">Sign in to Ekster</h2>
<form action="/session" method="post">
<div class="field">
<label class="label" for="url"></label>
<div class="control">
<input type="text" name="url" id="url" class="input" placeholder="https://example.com/">
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button type="submit" class="button is-info">Login</button>
</div>
</div>
</form>
{{ end }}
</div>
</section>
</body>
</html>

View File

@ -1,50 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ekster</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css">
</head>
<body>
<section class="section">
<div class="container">
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/">
Ekster
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="menu">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
{{ if .Session.LoggedIn }}
<div id="menu" class="navbar-menu">
<a class="navbar-item" href="/settings">
Settings
</a>
<a class="navbar-item" href="/logs">
Logs
</a>
<a class="navbar-item" href="{{ .Session.Me }}">
Profile
</a>
</div>
{{ end }}
</nav>
<h1 class="title">Ekster - Microsub server</h1>
<h2 class="subtitle">Logs</h2>
<p>Logs</p>
</div>
</section>
</body>
</html>

View File

@ -1,63 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ekster</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css">
</head>
<body>
<section class="section">
<div class="container">
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/">
Ekster
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="menu">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
{{ if .Session.LoggedIn }}
<div id="menu" class="navbar-menu">
<a class="navbar-item" href="/settings">
Settings
</a>
<a class="navbar-item" href="/logs">
Logs
</a>
<a class="navbar-item" href="{{ .Session.Me }}">
Profile
</a>
</div>
{{ end }}
</nav>
<h1 class="title">Ekster - Microsub server</h1>
{{ if .Session.LoggedIn }}
{{ end }}
<h2 class="subtitle">Channels</h2>
<div class="channels">
{{ range .Channels }}
<div class="channel box">
<div class="name">
<a href="/settings/channel?uid={{ .UID }}">
{{ .Name }}
</a>
</div>
</div>
{{ else }}
<div class="no-channels">No channels</div>
{{ end }}
</div>
</div>
</section>
</body>
</html>