diff --git a/README.md b/README.md index 7d2dcc04..3eddf9ef 100644 --- a/README.md +++ b/README.md @@ -146,13 +146,13 @@ Most user-visible data is stored in 4 tables in tootik's database: `shares.note` points to a row in `notes`. ``` -┌───────┐ ┌────────┐ ┌─────────┐ ┌─────────┐ ┏━━━━━━━━━┓ ┏━━━━━━━━━┓ -│ notes │ │ shares │ │ persons │ │ follows │ ┃ outbox ┃ ┃ inbox ┃ -├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ┣━━━━━━━━━┫ ┣━━━━━━━━━┫ -│object │ │note │ │actor │ │follower │ ┃activity ┃ ┃activity ┃ -│author │ │by │ │... │ │followed │ ┃sender ┃ ┃sender ┃ -│... │ │... │ │ │ │... │ ┃... ┃ ┃... ┃ -└───────┘ └────────┘ └─────────┘ └─────────┘ ┗━━━━━━━━━┛ ┗━━━━━━━━━┛ +┌───────┐ ┌────────┐ ┌─────────┐ ┌─────────┐ ┏━━━━━━━━┓ ┏━━━━━━━━┓ +│ notes │ │ shares │ │ persons │ │ follows │ ┃ outbox ┃ ┃ inbox ┃ +├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ┣━━━━━━━━┫ ┣━━━━━━━━┫ +│object │ │note │ │actor │ │follower │ ┃activity┃ ┃activity┃ +│author │ │by │ │... │ │followed │ ┃sender ┃ ┃sender ┃ +│... │ │... │ │ │ │... │ ┃... ┃ ┃... ┃ +└───────┘ └────────┘ └─────────┘ └─────────┘ ┗━━━━━━━━┛ ┗━━━━━━━━┛ ``` Federation happens through two tables, `inbox` and `outbox`. Both contain [Activity](https://pkg.go.dev/github.com/dimkr/tootik/ap#Activity) objects that represent actions performed by the users in `persons`. @@ -166,13 +166,14 @@ Federation happens through two tables, `inbox` and `outbox`. Both contain [Activ ┏━━━━━━━━┻━━━━━━━━━┓ ┃ front.Handler ┃ ┗━━━━━━━━━┳━━━━━━━━┛ -┌───────┐ ┌────────┐ ┌────┸────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ -│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ -├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├─────────┤ ├─────────┤ -│object │ │note │ │actor │ │follower │ │activity │ │activity │ -│author │ │by │ │... │ │followed │ │sender │ │sender │ -│... │ │... │ │ │ │... │ │... │ │... │ -└───────┘ └────────┘ └────┰────┘ └─────────┘ └─────────┘ └─────────┘ +┌───────┐ ┌────────┐ ┌────┸────┐ ┌─────────┐ ┌────────┐ ┌────────┐ +│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ +├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤ +│object │ │note │ │actor │ │follower │ │activity│ │activity│ +│author │ │by │ │... │ │followed │ │sender │ │sender │ +│... │ │... │ │ │ │... │ │... │ │... │ +└───────┘ └────────┘ └────┰────┘ └─────────┘ └────────┘ └────────┘ + ┃ ┏━━━━━━━┻━━━━━━┓ ┃ fed.Resolver ┃ ┗━━━━━━━━━━━━━━┛ @@ -189,13 +190,14 @@ Federation happens through two tables, `inbox` and `outbox`. Both contain [Activ ┌────────┴─────────┐ ┏━━━━━━━━━━━┥ front.Handler │ ┃ └┰────────┬───────┰┘ -┌───┸───┐ ┌──────┸─┐ ┌────┴────┐ ┌┸────────┐ ┌─────────┐ ┌─────────┐ -│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ -├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├─────────┤ ├─────────┤ -│object │ │note │ │actor │ │follower │ │activity │ │activity │ -│author │ │by │ │... │ │followed │ │sender │ │sender │ -│... │ │... │ │ │ │... │ │... │ │... │ -└───────┘ └────────┘ └────┬────┘ └─────────┘ └─────────┘ └─────────┘ +┌───┸───┐ ┌──────┸─┐ ┌────┴────┐ ┌┸────────┐ ┌────────┐ ┌────────┐ +│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ +├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤ +│object │ │note │ │actor │ │follower │ │activity│ │activity│ +│author │ │by │ │... │ │followed │ │sender │ │sender │ +│... │ │... │ │ │ │... │ │... │ │... │ +└───────┘ └────────┘ └────┬────┘ └─────────┘ └────────┘ └────────┘ + │ ┌───────┴──────┐ │ fed.Resolver │ └──────────────┘ @@ -217,16 +219,17 @@ In addition, Gemini requests can: ┌────────┴─────────┐ ┌───────────┤ front.Handler ┝━━━━━━━━━━━┓ │ └┬────────┬───────┬┘ ┃ -┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴────────┐ ┌─┸───────┐ ┌─────────┐ -│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ -├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├─────────┤ ├─────────┤ -│object │ │note │ │actor │ │follower │ │activity │ │activity │ -│author │ │by │ │... │ │followed │ │sender │ │sender │ -│... │ │... │ │ │ │... │ │... │ │... │ -└───────┘ └────────┘ └────┬────┘ └───────┰─┘ └┰────────┘ └─────────┘ - ┌───────┴──────┐ ┏┻━━━━┻━━━━━┓ - │ fed.Resolver │ ┃ fed.Queue ┃ - └──────────────┘ ┗━━━━━━━━━━━┛ +┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴────────┐ ┌─┸──────┐ ┌────────┐ +│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ +├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤ +│object │ │note │ │actor │ │follower │ │activity│ │activity│ +│author │ │by │ │... │ │followed │ │sender │ │sender │ +│... │ │... │ │ │ │... │ │... │ │... │ +└───────┘ └────────┘ └────┬────┘ └───────┰─┘ └┰───────┘ └────────┘ + │ ┏┻━━━━┻━━━━━┓ + ┌───────┴──────┐ ┃ fed.Queue ┃ + │ fed.Resolver │ ┗━━━━━━━━━━━┛ + └──────────────┘ ``` Each user action (post creation, post deletion, ...) is recorded as an [Activity](https://pkg.go.dev/github.com/dimkr/tootik/ap#Activity) object written to `outbox`. @@ -241,16 +244,17 @@ Each user action (post creation, post deletion, ...) is recorded as an [Activity ┌────────┴─────────┐ ┗━━━┳━━━━━┳━━━━━┛ ┌───────────┤ front.Handler ├──────╂────┐┃ │ └┬────────┬───────┬┘ ┃ │┃ -┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┸┐ ┌─┴┸──────┐ ┌─────────┐ -│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ -├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├─────────┤ ├─────────┤ -│object │ │note │ │actor │ │follower │ │activity │ │activity │ -│author │ │by │ │... │ │followed │ │sender │ │sender │ -│... │ │... │ │ │ │... │ │... │ │... │ -└───────┘ └────────┘ └────┬────┘ └───────┬─┘ └┬────────┘ └─────────┘ - ┌───────┴──────┐ ┌┴────┴─────┐ - │ fed.Resolver │ │ fed.Queue │ - └──────────────┘ └───────────┘ +┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┸┐ ┌─┴┸─────┐ ┌────────┐ +│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ +├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤ +│object │ │note │ │actor │ │follower │ │activity│ │activity│ +│author │ │by │ │... │ │followed │ │sender │ │sender │ +│... │ │... │ │ │ │... │ │... │ │... │ +└───────┘ └────────┘ └────┬────┘ └───────┬─┘ └┬───────┘ └────────┘ + │ ┌┴────┴─────┐ + ┌───────┴──────┐ │ fed.Queue │ + │ fed.Resolver │ └───────────┘ + └──────────────┘ ``` tootik may perform automatic actions in the name of the user: @@ -264,22 +268,22 @@ tootik may perform automatic actions in the name of the user: │ gemini.Listener │ │ outbox.Poller │ └────────┬────────┘ │ fed.Syncer │ ┌────────┴─────────┐ └───┬─────┬─────┘ ┏━━━━━━━━━━━━━━┓ - ┌───────────┤ front.Handler ├──────┼────┐│ ┏━━┫ fed.Listener ┣━━┓ - │ └┬────────┬───────┬┘ │ ││ ┃ ┗━━━━━┳━━━━━━━━┛ ┃ -┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴────┸─┐ ┌────┸────┐ ┃ -│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ ┃ -├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├─────────┤ ├─────────┤ ┃ -│object │ │note │ │actor │ │follower │ │activity │ │activity │ ┃ -│author │ │by │ │... │ │followed │ │sender │ │sender │ ┃ -│... │ │... │ │ │ │... │ │... │ │... │ ┃ -└───────┘ └────────┘ └────┬────┘ └───────┬─┘ └┬────────┘ └─────────┘ ┃ - ┌───────┴──────┐ ┌┴────┴─────┐ ┃ - │ fed.Resolver │ │ fed.Queue │ ┃ - └───────┰──────┘ └───────────┘ ┃ - ┃ ┃ - ┃ ┃ - ┃ ┃ - ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + ┌───────────┤ front.Handler ├──────┼────┐│ ┏━━┫ fed.Listener ┣━━━━━━┓ + │ └┬────────┬───────┬┘ │ ││ ┃ ┗━━━━━┳━━━━━━━━┛ ┃ +┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴────┸┐ ┌─────┸──┐ ┃ +│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ ┃ +├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤ ┃ +│object │ │note │ │actor │ │follower │ │activity│ │activity│ ┃ +│author │ │by │ │... │ │followed │ │sender │ │sender │ ┃ +│... │ │... │ │ │ │... │ │... │ │... │ ┃ +└───────┘ └────────┘ └────┬────┘ └───────┬─┘ └┬───────┘ └────────┘ ┃ + │ ┌┴────┴─────┐ ┃ + ┌───────┴──────┐ │ fed.Queue │ ┃ + │ fed.Resolver │ └───────────┘ ┃ + └───────┰──────┘ ┃ + ┃ ┃ + ┃ ┃ + ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ``` Requests from other servers are handled by [fed.Listener](https://pkg.go.dev/github.com/dimkr/tootik/fed#Listener), a HTTP server. @@ -294,22 +298,22 @@ In addition, [fed.Listener](https://pkg.go.dev/github.com/dimkr/tootik/fed#Liste │ gemini.Listener │ │ outbox.Poller │ └────────┬────────┘ │ fed.Syncer │ ┌────────┴─────────┐ └───┬─────┬─────┘ ┌──────────────┐ - ┌───────────┤ front.Handler ├──────┼────┐│ ┌──┤ fed.Listener ├──┐ - │ └┬────────┬───────┬┘ │ ││ │ └─────┬────────┘ │ -┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴────┴─┐ ┌────┴────┐ │ -│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ │ -├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├─────────┤ ├─────────┤ │ -│object │ │note │ │actor │ │follower │ │activity │ │activity │ │ -│author │ │by │ │... │ │followed │ │sender │ │sender │ │ -│... │ │... │ │ │ │... │ │... │ │... │ │ -└───┰───┘ └───┰────┘ └────┬────┘ └────┰──┬─┘ └┬────────┘ └──────┰──┘ │ - ┃ ┃ ┌───────┴──────┐ ┃ ┌┴────┴─────┐ ┏━━━━━┻━━━━━━━┓ │ - ┃ ┃ │ fed.Resolver │ ┃ │ fed.Queue │ ┃ inbox.Queue ┃ │ - ┃ ┃ └───────┬──────┘ ┃ └───────────┘ ┗━┳━┳━┳━━━━━━━┛ │ - ┃ ┃ │ ┗━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │ - ┃ ┗━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ - ┗━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ - └───────────────────────────────────────────────┘ + ┌───────────┤ front.Handler ├──────┼────┐│ ┌──┤ fed.Listener ├──────┐ + │ └┬────────┬───────┬┘ │ ││ │ └─────┬────────┘ │ +┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴────┴┐ ┌─────┴──┐ │ +│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ │ +├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤ │ +│object │ │note │ │actor │ │follower │ │activity│ │activity│ │ +│author │ │by │ │... │ │followed │ │sender │ │sender │ │ +│... │ │... │ │ │ │... │ │... │ │... │ │ +└───┰───┘ └───┰────┘ └────┬────┘ └────┰──┬─┘ └┬───────┘ └──────┰─┘ │ + ┃ ┃ │ ┃ ┌┴────┴─────┐ ┏━━━━┻━━━━━━━━┓ │ + ┃ ┃ ┌───────┴──────┐ ┃ │ fed.Queue │ ┃ inbox.Queue ┃ │ + ┃ ┃ │ fed.Resolver │ ┃ └───────────┘ ┗━┳━┳━┳━━━━━━━┛ │ + ┃ ┃ └───────┬──────┘ ┗━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │ + ┃ ┗━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ + ┗━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ + └───────────────────────────────────────────────────┘ ``` Once inserted into `inbox`, [inbox.Queue](https://pkg.go.dev/github.com/dimkr/tootik/inbox#Queue) processes the received activities: @@ -326,22 +330,22 @@ Once inserted into `inbox`, [inbox.Queue](https://pkg.go.dev/github.com/dimkr/to │ gemini.Listener │ │ outbox.Poller │ └────────┬────────┘ │ fed.Syncer │ ┌────────┴─────────┐ └───┬─────┬─────┘ ┌──────────────┐ - ┌───────────┤ front.Handler ├──────┼────┐│ ┌──┤ fed.Listener ├──┐ - │ └┬────────┬───────┬┘ │ ││ │ └─────┬────────┘ │ -┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴────┴─┐ ┌────┴────┐ │ -│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ │ -├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├─────────┤ ├─────────┤ │ -│object │ │note │ │actor │ │follower │ │activity │ │activity │ │ -│author │ │by │ │... │ │followed │ │sender │ │sender │ │ -│... │ │... │ │ │ │... │ │... │ │... │ │ -└───┬───┘ └───┬────┘ └────┬────┘ └────┬──┬─┘ └┬───────┰┘ └──────┬──┘ │ - │ │ ┌───────┴──────┐ │ ┌┴────┴─────┐ ┃ ┌─────┴───────┐ │ - │ │ │ fed.Resolver │ │ │ fed.Queue │ ┗━━━┥ inbox.Queue │ │ - │ │ └───────┬──────┘ │ └───────────┘ └─┬─┬─┬───────┘ │ - │ │ │ └─────────────────────┘ │ │ │ - │ └───────────┼───────────────────────────────────┘ │ │ - └─────────────────────┼─────────────────────────────────────┘ │ - └───────────────────────────────────────────────┘ + ┌───────────┤ front.Handler ├──────┼────┐│ ┌──┤ fed.Listener ├──────┐ + │ └┬────────┬───────┬┘ │ ││ │ └─────┬────────┘ │ +┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴────┴┐ ┌─────┴──┐ │ +│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ │ +├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤ │ +│object │ │note │ │actor │ │follower │ │activity│ │activity│ │ +│author │ │by │ │... │ │followed │ │sender │ │sender │ │ +│... │ │... │ │ │ │... │ │... │ │... │ │ +└───┬───┘ └───┬────┘ └────┬────┘ └────┬──┬─┘ └┬──────┰┘ └──────┬─┘ │ + │ │ │ │ ┌┴────┴─────┐┃ ┌────┴────────┐ │ + │ │ ┌───────┴──────┐ │ │ fed.Queue │┗━━━━┥ inbox.Queue │ │ + │ │ │ fed.Resolver │ │ └───────────┘ └─┬─┬─┬───────┘ │ + │ │ └───────┬──────┘ └─────────────────────┘ │ │ │ + │ └───────────┼───────────────────────────────────┘ │ │ + └─────────────────────┼─────────────────────────────────────┘ │ + └───────────────────────────────────────────────────┘ ``` Sometimes, a received or newly created local [Activity](https://pkg.go.dev/github.com/dimkr/tootik/ap#Activity) is forwarded to the followers of a local user: @@ -354,27 +358,57 @@ Sometimes, a received or newly created local [Activity](https://pkg.go.dev/githu │ gemini.Listener │ │ outbox.Poller │ └────────┬────────┘ │ fed.Syncer │ ┌────────┴─────────┐ └───┬─────┬─────┘ ┌──────────────┐ - ┌───────────┤ front.Handler ├──────┼────┐│ ┌──┤ fed.Listener ├──┐ - │ └┬────────┬───────┬┘ │ ││ │ └─────┬────────┘ │ -┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴────┴─┐ ┌────┴────┐ │ -│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ │ -├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├─────────┤ ├─────────┤ │ -│object │ │note │ │actor │ │follower │ │activity │ │activity │ │ -│author │ │by │ │... │ │followed │ │sender │ │sender │ │ -│... │ │... │ │ │ │... │ │... │ │... │ │ -└───┬───┘ └───┬────┘ └────┬────┘ └────┬──┬─┘ └┬───────┬┘ └──────┬──┘ │ - │ │ ┌───────┴──────┐ │ ┌┴────┴─────┐ │ ┌─────┴───────┐ │ - │ │ │ fed.Resolver │ │ │ fed.Queue │ └───┤ inbox.Queue │ │ - │ │ └───────┬─┰────┘ │ └───────────┘ └─┬─┬─┬─┰─────┘ │ - │ │ │ ┃ └─────────────────────┘ │ │ ┃ │ - │ └───────────┼─╂─────────────────────────────────┘ │ ┃ │ - └─────────────────────┼─╂───────────────────────────────────┘ ┃ │ - └─╂─────────────────────────────────────╂───────┘ + ┌───────────┤ front.Handler ├──────┼────┐│ ┌──┤ fed.Listener ├──────┐ + │ └┬────────┬───────┬┘ │ ││ │ └─────┬────────┘ │ +┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴────┴┐ ┌─────┴──┐ │ +│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ │ +├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤ │ +│object │ │note │ │actor │ │follower │ │activity│ │activity│ │ +│author │ │by │ │... │ │followed │ │sender │ │sender │ │ +│... │ │... │ │ │ │... │ │... │ │... │ │ +└───┬───┘ └───┬────┘ └────┬────┘ └────┬──┬─┘ └┬──────┬┘ └──────┬─┘ │ + │ │ │ │ ┌┴────┴─────┐│ ┌────┴────────┐ │ + │ │ ┌───────┴──────┐ │ │ fed.Queue │└────┤ inbox.Queue │ │ + │ │ │ fed.Resolver │ │ └───────────┘ └─┬─┬─┬─┰─────┘ │ + │ │ └───────┬─┰────┘ └─────────────────────┘ │ │ ┃ │ + │ └───────────┼─╂─────────────────────────────────┘ │ ┃ │ + └─────────────────────┼─╂───────────────────────────────────┘ ┃ │ + └─╂─────────────────────────────────────╂───────────┘ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ``` To display details like the user's name and speed up the verification of future incoming replies, [inbox.Queue](https://pkg.go.dev/github.com/dimkr/tootik/inbox#Queue) uses [Resolver](https://pkg.go.dev/github.com/dimkr/tootik/fed#Resolver) to fetch the [Actor](https://pkg.go.dev/github.com/dimkr/tootik/ap#Actor) objects of mentioned users (if needed). +``` + ┌───────────────┐ + ┌─────────────────┐ │ outbox.Mover │ + │ gemini.Listener │ │ outbox.Poller │ + └────────┬────────┘ │ fed.Syncer │ + ┌────────┴─────────┐ └───┬─────┬─────┘ ┌──────────────┐ + ┌───────────┤ front.Handler ├──────┼────┐│ ┌──┤ fed.Listener ├──────┐ + │ └┬────────┬───────┬┘ │ ││ │ └─────┬────────┘ │ +┌───┴───┐ ┌──────┴─┐ ┌────┴────┐ ┌┴───────┴┐ ┌─┴┴────┴┐ ┌─────┴──┐ ┏━━━━━━━━┓ │ +│ notes │ │ shares │ │ persons │ │ follows │ │ outbox │ │ inbox │ ┃ feed ┃ │ +├───────┤ ├────────┤ ├─────────┤ ├─────────┤ ├────────┤ ├────────┤ ┣━━━━━━━━┫ │ +│object │ │note │ │actor │ │follower │ │activity│ │activity│ ┃follower┃ │ +│author │ │by │ │... │ │followed │ │sender │ │sender │ ┃note ┃ │ +│... │ │... │ │ │ │... │ │... │ │... │ ┃... ┃ │ +└─┰─┬───┘ └─┰─┬────┘ └──┰─┬────┘ └──┰─┬──┬─┘ └┬──────┬┘ └──────┬─┘ ┗━━━━━━┳━┛ │ + ┃ │ ┃ │ ┏━━━━━━━┛ │ ┃ │ ┌┴────┴─────┐│ ┌────┴────────┐ ┃ │ + ┃ │ ┃ │ ┃ ┌───────┴──────┐ ┃ │ │ fed.Queue │└────┤ inbox.Queue │ ┃ │ + ┃ │ ┃ │ ┃ │ fed.Resolver │ ┃ │ └───────────┘ └─┬─┬─┬─┬─────┘ ┃ │ + ┃ │ ┃ │ ┃ └───────┬─┬────┘ ┃ └─────────────────────┘ │ │ │ ┃ │ + ┃ │ ┃ └─╂─────────┼─┼───────╂─────────────────────────┘ │ │ ┃ │ + ┃ └───────╂───╂─────────┼─┼───────╂───────────────────────────┘ │ ┃ │ + ┃ ┃ ┃ └─┼───────╂─────────────────────────────┼───────╂───┘ +┏━┻━━━━━━━━━┻━━━┻━━━┓ └───────╂─────────────────────────────┘ ┃ +┃ inbox.FeedUpdater ┣━━━━━━━━━━━━━━━┛ ┃ +┗━━━━━━━━━┳━━━━━━━━━┛ ┃ + ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +``` + +To speed up each user's feed, [inbox.FeedUpdater](https://pkg.go.dev/github.com/dimkr/tootik/inbox#FeedUpdater) periodically appends rows to the `feed` table. This table holds all information that appears in the user's feed: posts written or shared by followed users, author information and more, eliminating the need for `join` queries, slow filtering by post visibility, deduplication and sorting by time when a user views their feed. This table is indexed by user and time, allowing fast querying of a single feed page for a particular user. + ## More Documentation * [Setup guide](SETUP.md) diff --git a/SETUP.md b/SETUP.md index 154f1894..44453a70 100644 --- a/SETUP.md +++ b/SETUP.md @@ -106,7 +106,7 @@ If you have a graphical web browser and a Gemini client that configures itself a 11. Register by creating a client certificate or clicking "Sign in" and use "View profile" to verify that your instance is able to "discover" users on other servers. -Once a user is discovered, you can follow this user and your instance should start receiving new posts by this user. They should appear under your user's inbox ("My feed") and the user's profile. +Once a user is discovered, you can follow this user and your instance should start receiving new posts by this user. They should appear under your user's inbox ("My feed") after a while (`FeedUpdateInterval`) and the user's profile. **If you don't see any posts, check tootik's output.** diff --git a/cfg/cfg.go b/cfg/cfg.go index c3851971..fb623711 100644 --- a/cfg/cfg.go +++ b/cfg/cfg.go @@ -108,11 +108,14 @@ type Config struct { FollowersSyncBatchSize int FollowersSyncInterval time.Duration + FeedUpdateInterval time.Duration + NotesTTL time.Duration InvisiblePostsTTL time.Duration DeliveryTTL time.Duration SharesTTL time.Duration ActorTTL time.Duration + FeedTTL time.Duration } // FillDefaults replaces missing or invalid settings with defaults. @@ -364,6 +367,10 @@ func (c *Config) FillDefaults() { c.FollowersSyncInterval = time.Hour * 24 * 3 } + if c.FeedUpdateInterval <= 0 { + c.FeedUpdateInterval = time.Minute * 10 + } + if c.NotesTTL <= 0 { c.NotesTTL = time.Hour * 24 * 30 } @@ -383,4 +390,8 @@ func (c *Config) FillDefaults() { if c.ActorTTL <= 0 { c.ActorTTL = time.Hour * 24 * 7 } + + if c.FeedTTL <= 0 { + c.FeedTTL = time.Hour * 24 * 7 + } } diff --git a/cmd/tootik/main.go b/cmd/tootik/main.go index 87ef6f10..7f7db2e8 100644 --- a/cmd/tootik/main.go +++ b/cmd/tootik/main.go @@ -442,6 +442,15 @@ func main() { Run(context.Context) error } }{ + { + "feed", + cfg.FeedUpdateInterval, + &inbox.FeedUpdater{ + Domain: *domain, + Config: &cfg, + DB: db, + }, + }, { "poller", pollResultsUpdateInterval, @@ -495,10 +504,12 @@ func main() { for { log.Info("Running periodic job", "job", job.Name) + start := time.Now() if err := job.Runner.Run(ctx); err != nil { log.Error("Periodic job has failed", "job", job.Name, "error", err) break } + log.Info("Done running periodic job", "job", job.Name, "duration", time.Since(start).String()) select { case <-ctx.Done(): diff --git a/data/garbage.go b/data/garbage.go index 4f3169da..6049d56b 100644 --- a/data/garbage.go +++ b/data/garbage.go @@ -78,5 +78,9 @@ func (gc *GarbageCollector) Run(ctx context.Context) error { return fmt.Errorf("failed to remove idle actors: %w", err) } + if _, err := gc.DB.ExecContext(ctx, `delete from feed where inserted < ?`, now.Add(-gc.Config.FeedTTL).Unix()); err != nil { + return fmt.Errorf("failed to tri feed: %w", err) + } + return nil } diff --git a/fed/resolve.go b/fed/resolve.go index ed9a7272..099985c6 100644 --- a/fed/resolve.go +++ b/fed/resolve.go @@ -140,11 +140,19 @@ func (r *Resolver) tryResolveOrCache(ctx context.Context, log *slog.Logger, db * } func deleteActor(ctx context.Context, log *slog.Logger, db *sql.DB, id string) { - if _, err := db.ExecContext(ctx, `delete from notesfts where id in (select id from notes where author = ?)`, id); err != nil { + if _, err := db.ExecContext(ctx, `delete from notesfts where exists (select 1 from notes where notes.author = ? and notesfts.id = notes.id)`, id); err != nil { log.Warn("Failed to delete notes by actor", "id", id, "error", err) } - if _, err := db.ExecContext(ctx, `delete from shares where by = $1 or note in (select id from notes where author = $1)`, id); err != nil { + if _, err := db.ExecContext(ctx, `delete from shares where by = $1 or exists (select 1 from notes where notes.author = ? and notes.id = shares.note)`, id); err != nil { + log.Warn("Failed to delete shares by actor", "id", id, "error", err) + } + + if _, err := db.ExecContext(ctx, `delete from feed where sharer->>'$.id' = ?`, id); err != nil { + log.Warn("Failed to delete shares by actor", "id", id, "error", err) + } + + if _, err := db.ExecContext(ctx, `delete from feed where author->>'$.id' = ?`, id); err != nil { log.Warn("Failed to delete shares by actor", "id", id, "error", err) } @@ -327,7 +335,12 @@ func (r *Resolver) tryResolve(ctx context.Context, log *slog.Logger, db *sql.DB, return nil, cachedActor, fmt.Errorf("%s does not match %s", actor.ID, profile) } - if _, err := db.ExecContext( + tx, err := db.BeginTx(ctx, nil) + if err != nil { + return nil, cachedActor, fmt.Errorf("failed to cache %s: %w", actor.ID, err) + } + + if _, err := tx.ExecContext( ctx, `INSERT INTO persons(id, actor, fetched) VALUES($1, $2, UNIXEPOCH()) ON CONFLICT(id) DO UPDATE SET actor = $2, updated = UNIXEPOCH()`, actor.ID, @@ -336,6 +349,28 @@ func (r *Resolver) tryResolve(ctx context.Context, log *slog.Logger, db *sql.DB, return nil, cachedActor, fmt.Errorf("failed to cache %s: %w", actor.ID, err) } + if _, err := tx.ExecContext( + ctx, + `UPDATE feed SET author = ? WHERE author->>'$.id' = ?`, + string(body), + actor.ID, + ); err != nil { + return nil, cachedActor, fmt.Errorf("failed to cache %s: %w", actor.ID, err) + } + + if _, err := tx.ExecContext( + ctx, + `UPDATE feed SET sharer = ? WHERE sharer->>'$.id' = ?`, + string(body), + actor.ID, + ); err != nil { + return nil, cachedActor, fmt.Errorf("failed to cache %s: %w", actor.ID, err) + } + + if err := tx.Commit(); err != nil { + return nil, cachedActor, fmt.Errorf("failed to cache %s: %w", actor.ID, err) + } + if actor.Published == nil && cachedActor != nil && cachedActor.Published != nil { actor.Published = cachedActor.Published } else if actor.Published == nil && (cachedActor == nil || cachedActor.Published == nil) { diff --git a/front/mentions.go b/front/mentions.go index 2de75bf5..7e332d54 100644 --- a/front/mentions.go +++ b/front/mentions.go @@ -18,7 +18,6 @@ package front import ( "database/sql" - "time" "github.com/dimkr/tootik/front/text" ) @@ -35,60 +34,19 @@ func (h *Handler) mentions(w text.Writer, r *request, args ...string) { "📞 Mentions", func(offset int) (*sql.Rows, error) { return r.Query(` - select object, actor, sharer, inserted from - ( - select notes.id, notes.object, persons.actor, notes.inserted, null as sharer from - follows - join - persons - on - persons.id = follows.followed - join - notes - on - notes.author = follows.followed and - ( - $1 in (notes.cc0, notes.to0, notes.cc1, notes.to1, notes.cc2, notes.to2) or - (notes.to2 is not null and exists (select 1 from json_each(notes.object->'$.to') where value = $1)) or - (notes.cc2 is not null and exists (select 1 from json_each(notes.object->'$.cc') where value = $1)) - ) - where - follows.follower = $1 and - notes.inserted >= $2 - union all - select notes.id, notes.object, authors.actor, shares.inserted, sharers.actor as sharer from - follows - join - shares - on - shares.by = follows.followed - join - notes - on - notes.id = shares.note and - ( - $1 in (notes.cc0, notes.to0, notes.cc1, notes.to1, notes.cc2, notes.to2) or - (notes.to2 is not null and exists (select 1 from json_each(notes.object->'$.to') where value = $1)) or - (notes.cc2 is not null and exists (select 1 from json_each(notes.object->'$.cc') where value = $1)) - ) - join - persons authors - on - authors.id = notes.author - join - persons sharers - on - sharers.id = follows.followed - where - follows.follower = $1 and - shares.inserted >= $2 - ) + select note, author, sharer, inserted from + feed + where + follower = $1 and + ( + exists (select 1 from json_each(note->'$.to') where value = $1) or + exists (select 1 from json_each(note->'$.cc') where value = $1) + ) order by inserted desc - limit $3 - offset $4`, + limit $2 + offset $3`, r.User.ID, - time.Now().Add(-time.Hour*24*7).Unix(), h.Config.PostsPerPage, offset, ) diff --git a/front/users.go b/front/users.go index 199f7b5c..7cdd2a74 100644 --- a/front/users.go +++ b/front/users.go @@ -18,7 +18,6 @@ package front import ( "database/sql" - "time" "github.com/dimkr/tootik/front/text" ) @@ -35,76 +34,15 @@ func (h *Handler) users(w text.Writer, r *request, args ...string) { "📻 My Feed", func(offset int) (*sql.Rows, error) { return r.Query(` - select object, actor, sharer, inserted from - ( - select id, object, actor, inserted, null as sharer from - ( - select notes.id, notes.object, persons.actor, notes.inserted from - follows - join - persons - on - persons.id = follows.followed - join - notes - on - notes.author = follows.followed and - ( - notes.public = 1 or - persons.actor->>'$.followers' in (notes.cc0, notes.to0, notes.cc1, notes.to1, notes.cc2, notes.to2) or - $1 in (notes.cc0, notes.to0, notes.cc1, notes.to1, notes.cc2, notes.to2) or - (notes.to2 is not null and exists (select 1 from json_each(notes.object->'$.to') where value = persons.actor->>'$.followers' or value = $1)) or - (notes.cc2 is not null and exists (select 1 from json_each(notes.object->'$.cc') where value = persons.actor->>'$.followers' or value = $1)) - ) - where - follows.follower = $1 and - notes.inserted >= $2 - union - select notes.id, notes.object, authors.actor, notes.inserted from - notes myposts - join - notes - on - notes.object->>'$.inReplyTo' = myposts.id - join - persons authors - on - authors.id = notes.author - where - myposts.author = $1 and - notes.author != $1 and - notes.inserted >= $2 - ) - union all - select notes.id, notes.object, authors.actor, shares.inserted, sharers.actor as sharer from - follows - join - shares - on - shares.by = follows.followed - join - notes - on - notes.id = shares.note - join - persons authors - on - authors.id = notes.author - join - persons sharers - on - sharers.id = follows.followed - where - follows.follower = $1 and - shares.inserted >= $2 and - notes.public = 1 - ) + select note, author, sharer, inserted from + feed + where + follower = $1 order by inserted desc - limit $3 - offset $4`, + limit $2 + offset $3`, r.User.ID, - time.Now().Add(-time.Hour*24).Unix(), h.Config.PostsPerPage, offset, ) diff --git a/inbox/feed.go b/inbox/feed.go new file mode 100644 index 00000000..62d6996b --- /dev/null +++ b/inbox/feed.go @@ -0,0 +1,114 @@ +/* +Copyright 2024 Dima Krasner + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package inbox + +import ( + "context" + "database/sql" + "fmt" + "github.com/dimkr/tootik/cfg" +) + +type FeedUpdater struct { + Domain string + Config *cfg.Config + DB *sql.DB +} + +func (u FeedUpdater) Run(ctx context.Context) error { + since := int64(0) + var ts sql.NullInt64 + if err := u.DB.QueryRowContext(ctx, `select max(inserted) from feed`).Scan(&ts); err != nil { + return err + } else if ts.Valid { + since = ts.Int64 + } + + if _, err := u.DB.ExecContext( + ctx, + ` + insert into feed(follower, note, author, sharer, inserted) + select follows.follower, notes.object as note, persons.actor as author, null as sharer, notes.inserted from + follows + join + persons + on + persons.id = follows.followed + join + notes + on + notes.author = follows.followed and + ( + notes.public = 1 or + persons.actor->>'$.followers' in (notes.cc0, notes.to0, notes.cc1, notes.to1, notes.cc2, notes.to2) or + follows.follower in (notes.cc0, notes.to0, notes.cc1, notes.to1, notes.cc2, notes.to2) or + (notes.to2 is not null and exists (select 1 from json_each(notes.object->'$.to') where value = persons.actor->>'$.followers' or value = follows.follower)) or + (notes.cc2 is not null and exists (select 1 from json_each(notes.object->'$.cc') where value = persons.actor->>'$.followers' or value = follows.follower)) + ) + where + follows.follower like $1 and + notes.inserted >= $2 and + not exists (select 1 from feed where feed.follower = follows.follower and feed.note->>'$.id' = notes.id and feed.sharer is null) + union + select myposts.author as follower, notes.object as note, authors.actor as author, null as sharer, notes.inserted from + notes myposts + join + notes + on + notes.object->>'$.inReplyTo' = myposts.id + join + persons authors + on + authors.id = notes.author + where + notes.author != myposts.author and + notes.inserted >= $2 and + myposts.author like $1 and + not exists (select 1 from feed where feed.follower = notes.author and feed.note->>'$.id' = notes.id and feed.sharer is null) + union all + select follows.follower, notes.object as note, authors.actor as author, sharers.actor as sharer, shares.inserted from + follows + join + shares + on + shares.by = follows.followed + join + notes + on + notes.id = shares.note + join + persons authors + on + authors.id = notes.author + join + persons sharers + on + sharers.id = follows.followed + where + notes.public = 1 and + shares.inserted >= $2 and + follows.follower like $1 and + not exists (select 1 from feed where feed.follower = follows.follower and feed.note->>'$.id' = notes.id and feed.sharer->>'$.id' = sharers.id) + `, + fmt.Sprintf("https://%s/%%", u.Domain), + since, + ); err != nil { + return err + } + + return nil +} diff --git a/inbox/queue.go b/inbox/queue.go index f7911b19..342c7cb5 100644 --- a/inbox/queue.go +++ b/inbox/queue.go @@ -78,9 +78,23 @@ func processCreateActivity[T ap.RawActivity](ctx context.Context, q *Queue, log return fmt.Errorf("failed to check of %s is a duplicate: %w", post.ID, err) } else if err == nil { if sender.ID == post.Audience && !audience.Valid { - if _, err := q.DB.ExecContext(ctx, `update notes set object = json_set(object, '$.audience', ?) where id = ? and object->>'$.audience' is null`, post.Audience, post.ID); err != nil { + tx, err := q.DB.BeginTx(ctx, nil) + if err != nil { + return fmt.Errorf("cannot set %s audience: %w", post.ID, err) + } + defer tx.Rollback() + + if _, err := tx.ExecContext(ctx, `update notes set object = json_set(object, '$.audience', ?) where id = ? and object->>'$.audience' is null`, post.Audience, post.ID); err != nil { return fmt.Errorf("failed to set %s audience to %s: %w", post.ID, audience.String, err) } + + if _, err := tx.ExecContext(ctx, `update feed set note = json_set(note, '$.audience', ?) where note->>'$.id' = ? and note->>'$.audience' is null`, post.Audience, post.ID); err != nil { + return fmt.Errorf("failed to set %s audience to %s: %w", post.ID, audience.String, err) + } + + if err := tx.Commit(); err != nil { + return fmt.Errorf("cannot set %s audience: %w", post.ID, err) + } } log.Debug("Post is a duplicate") @@ -199,6 +213,9 @@ func processActivity[T ap.RawActivity](ctx context.Context, q *Queue, log *slog. if _, err := tx.ExecContext(ctx, `delete from shares where note = ?`, deleted); err != nil { return fmt.Errorf("cannot delete %s: %w", deleted, err) } + if _, err := tx.ExecContext(ctx, `delete from feed where note->>'$.id' = ?`, deleted); err != nil { + return fmt.Errorf("cannot delete %s: %w", deleted, err) + } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to delete %s: %w", deleted, err) @@ -414,6 +431,15 @@ func processActivity[T ap.RawActivity](ctx context.Context, q *Queue, log *slog. } } + if _, err := tx.ExecContext( + ctx, + `update feed set note = ? where note->>'$.id' = ?`, + post, + post.ID, + ); err != nil { + return fmt.Errorf("failed to update post %s: %w", post.ID, err) + } + if err := outbox.ForwardActivity(ctx, q.Domain, q.Config, log, tx, post, activity, rawActivity); err != nil { return fmt.Errorf("failed to forward update post %s: %w", post.ID, err) } diff --git a/migrations/031_feed.go b/migrations/031_feed.go new file mode 100644 index 00000000..6bf9be7d --- /dev/null +++ b/migrations/031_feed.go @@ -0,0 +1,34 @@ +package migrations + +import ( + "context" + "database/sql" +) + +func feed(ctx context.Context, domain string, tx *sql.Tx) error { + if _, err := tx.ExecContext(ctx, `CREATE TABLE feed(follower STRING NOT NULL, note STRING NOT NULL, author STRING NOT NULL, sharer STRING, inserted INTEGER NOT NULL)`); err != nil { + return err + } + + if _, err := tx.ExecContext(ctx, `CREATE INDEX feedfollowerinserted ON feed(follower, inserted)`); err != nil { + return err + } + + if _, err := tx.ExecContext(ctx, `CREATE INDEX feedinserted ON feed(inserted)`); err != nil { + return err + } + + if _, err := tx.ExecContext(ctx, `CREATE INDEX feednote ON feed(note->>'$.id')`); err != nil { + return err + } + + if _, err := tx.ExecContext(ctx, `CREATE INDEX feedauthorid ON feed(author->>'$.id')`); err != nil { + return err + } + + if _, err := tx.ExecContext(ctx, `CREATE INDEX feedshareid ON feed(sharer->>'$.id') WHERE sharer->>'$.id' IS NOT NULL`); err != nil { + return err + } + + return nil +} diff --git a/outbox/delete.go b/outbox/delete.go index 48fd1d16..2f14d48d 100644 --- a/outbox/delete.go +++ b/outbox/delete.go @@ -90,6 +90,14 @@ func Delete(ctx context.Context, domain string, cfg *cfg.Config, log *slog.Logge return fmt.Errorf("failed to delete note: %w", err) } + if _, err := tx.ExecContext( + ctx, + `DELETE FROM feed WHERE note->>'$.id' = ?`, + note.ID, + ); err != nil { + return fmt.Errorf("failed to delete note: %w", err) + } + if _, err := tx.ExecContext( ctx, `INSERT INTO outbox (activity, sender) VALUES (?,?)`, diff --git a/outbox/undo.go b/outbox/undo.go index 6e602f18..22cf467c 100644 --- a/outbox/undo.go +++ b/outbox/undo.go @@ -60,6 +60,15 @@ func Undo(ctx context.Context, domain string, db *sql.DB, activity *ap.Activity) return fmt.Errorf("failed to remove share: %w", err) } + if _, err := tx.ExecContext( + ctx, + `DELETE FROM feed WHERE note->>'$.id' = ? AND sharer->>'$.id' = ?`, + noteID, + activity.Actor, + ); err != nil { + return fmt.Errorf("failed to remove share: %w", err) + } + if _, err := tx.ExecContext( ctx, `INSERT INTO outbox (activity, sender) VALUES(?,?)`, diff --git a/outbox/update.go b/outbox/update.go index 225d48fa..711dcad3 100644 --- a/outbox/update.go +++ b/outbox/update.go @@ -73,6 +73,15 @@ func UpdateNote(ctx context.Context, domain string, cfg *cfg.Config, log *slog.L return fmt.Errorf("failed to update note: %w", err) } + if _, err := tx.ExecContext( + ctx, + `UPDATE feed SET note = ? WHERE note->>'$.id' = ?`, + ¬e, + note.ID, + ); err != nil { + return fmt.Errorf("failed to update note: %w", err) + } + if _, err := tx.ExecContext( ctx, `INSERT INTO outbox (activity, sender) VALUES(?,?)`, diff --git a/test/edit_test.go b/test/edit_test.go index c68cc539..48eb9fba 100644 --- a/test/edit_test.go +++ b/test/edit_test.go @@ -19,6 +19,7 @@ package test import ( "context" "fmt" + "github.com/dimkr/tootik/inbox" "github.com/dimkr/tootik/outbox" "github.com/stretchr/testify/assert" "log/slog" @@ -36,6 +37,8 @@ func TestEdit_Throttling(t *testing.T) { follow := server.Handle("/users/follow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), follow) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "No posts.") assert.NotContains(users, "Hello world") @@ -48,6 +51,12 @@ func TestEdit_Throttling(t *testing.T) { edit := server.Handle(fmt.Sprintf("/users/edit/%s?Hello%%20followers", id), server.Bob) assert.Equal("40 Please try again later\r\n", edit) + users = server.Handle("/users", server.Alice) + assert.Contains(users, "No posts.") + assert.NotContains(users, "Hello world") + + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users = server.Handle("/users", server.Alice) assert.NotContains(users, "No posts.") assert.Contains(users, "Hello world") @@ -62,6 +71,8 @@ func TestEdit_HappyFlow(t *testing.T) { follow := server.Handle("/users/follow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), follow) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "No posts.") assert.NotContains(users, "Hello followers") @@ -77,6 +88,12 @@ func TestEdit_HappyFlow(t *testing.T) { edit := server.Handle(fmt.Sprintf("/users/edit/%s?Hello%%20followers", id), server.Bob) assert.Equal(fmt.Sprintf("30 /users/view/%s\r\n", id), edit) + users = server.Handle("/users", server.Alice) + assert.Contains(users, "No posts.") + assert.NotContains(users, "Hello followers") + + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users = server.Handle("/users", server.Alice) assert.NotContains(users, "No posts.") assert.Contains(users, "Hello followers") @@ -102,6 +119,8 @@ func TestEdit_EmptyContent(t *testing.T) { follow := server.Handle("/users/follow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), follow) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "No posts.") assert.NotContains(users, "Hello world") @@ -117,6 +136,12 @@ func TestEdit_EmptyContent(t *testing.T) { edit := server.Handle(fmt.Sprintf("/users/edit/%s?", id), server.Bob) assert.Equal("10 Post content\r\n", edit) + users = server.Handle("/users", server.Alice) + assert.Contains(users, "No posts.") + assert.NotContains(users, "Hello world") + + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users = server.Handle("/users", server.Alice) assert.NotContains(users, "No posts.") assert.Contains(users, "Hello world") @@ -131,6 +156,8 @@ func TestEdit_LongContent(t *testing.T) { follow := server.Handle("/users/follow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), follow) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "No posts.") assert.NotContains(users, "Hello world") @@ -146,6 +173,12 @@ func TestEdit_LongContent(t *testing.T) { edit := server.Handle(fmt.Sprintf("/users/edit/%s?aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", id), server.Bob) assert.Equal("40 Post is too long\r\n", edit) + users = server.Handle("/users", server.Alice) + assert.Contains(users, "No posts.") + assert.NotContains(users, "Hello world") + + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users = server.Handle("/users", server.Alice) assert.NotContains(users, "No posts.") assert.Contains(users, "Hello world") @@ -160,6 +193,8 @@ func TestEdit_InvalidEscapeSequence(t *testing.T) { follow := server.Handle("/users/follow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), follow) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "No posts.") assert.NotContains(users, "Hello world") @@ -175,6 +210,12 @@ func TestEdit_InvalidEscapeSequence(t *testing.T) { edit := server.Handle(fmt.Sprintf("/users/edit/%s?Hello%%zzworld", id), server.Bob) assert.Equal("40 Bad input\r\n", edit) + users = server.Handle("/users", server.Alice) + assert.Contains(users, "No posts.") + assert.NotContains(users, "Hello world") + + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users = server.Handle("/users", server.Alice) assert.NotContains(users, "No posts.") assert.Contains(users, "Hello world") @@ -189,6 +230,8 @@ func TestEdit_NoSuchPost(t *testing.T) { follow := server.Handle("/users/follow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), follow) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "No posts.") assert.NotContains(users, "Hello world") @@ -199,6 +242,12 @@ func TestEdit_NoSuchPost(t *testing.T) { edit := server.Handle("/users/edit/x?Hello%20followers", server.Bob) assert.Equal("40 Error\r\n", edit) + users = server.Handle("/users", server.Alice) + assert.Contains(users, "No posts.") + assert.NotContains(users, "Hello world") + + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users = server.Handle("/users", server.Alice) assert.NotContains(users, "No posts.") assert.Contains(users, "Hello world") @@ -213,6 +262,8 @@ func TestEdit_UnauthenticatedUser(t *testing.T) { follow := server.Handle("/users/follow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), follow) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "No posts.") assert.NotContains(users, "Hello world") @@ -225,6 +276,12 @@ func TestEdit_UnauthenticatedUser(t *testing.T) { edit := server.Handle(fmt.Sprintf("/users/edit/%s?Hello%%20followers", id), nil) assert.Equal("30 /users\r\n", edit) + users = server.Handle("/users", server.Alice) + assert.Contains(users, "No posts.") + assert.NotContains(users, "Hello world") + + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users = server.Handle("/users", server.Alice) assert.NotContains(users, "No posts.") assert.Contains(users, "Hello world") @@ -326,6 +383,8 @@ func TestEdit_AddMention(t *testing.T) { assert := assert.New(t) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + lines := strings.Split(server.Handle("/users", server.Alice), "\n") assert.Contains(lines, "No posts.") assert.NotContains(lines, "> Hello world") @@ -358,6 +417,8 @@ func TestEdit_RemoveMention(t *testing.T) { assert := assert.New(t) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + lines := strings.Split(server.Handle("/users", server.Alice), "\n") assert.Contains(lines, "No posts.") assert.NotContains(lines, "> Hello @alice") @@ -390,6 +451,8 @@ func TestEdit_KeepMention(t *testing.T) { assert := assert.New(t) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + lines := strings.Split(server.Handle("/users", server.Alice), "\n") assert.Contains(lines, "No posts.") assert.NotContains(lines, "> Hello @alice") diff --git a/test/follow_test.go b/test/follow_test.go index b9c36f17..8d672623 100644 --- a/test/follow_test.go +++ b/test/follow_test.go @@ -17,7 +17,9 @@ limitations under the License. package test import ( + "context" "fmt" + "github.com/dimkr/tootik/inbox" "github.com/stretchr/testify/assert" "strings" "testing" @@ -32,6 +34,8 @@ func TestFollow_PostToFollowers(t *testing.T) { follow := server.Handle("/users/follow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), follow) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "No posts.") assert.NotContains(users, "Hello world") @@ -39,6 +43,12 @@ func TestFollow_PostToFollowers(t *testing.T) { whisper := server.Handle("/users/whisper?Hello%20world", server.Bob) assert.Regexp(`^30 /users/view/\S+\r\n$`, whisper) + users = server.Handle("/users", server.Alice) + assert.Contains(users, "No posts.") + assert.NotContains(users, "Hello world") + + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users = server.Handle("/users", server.Alice) assert.NotContains(users, "No posts.") assert.Contains(users, "Hello world") @@ -50,6 +60,8 @@ func TestFollow_PostToFollowersBeforeFollow(t *testing.T) { assert := assert.New(t) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "No posts.") assert.NotContains(users, "Hello world") @@ -60,6 +72,12 @@ func TestFollow_PostToFollowersBeforeFollow(t *testing.T) { follow := server.Handle("/users/follow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), follow) + users = server.Handle("/users", server.Alice) + assert.Contains(users, "No posts.") + assert.NotContains(users, "Hello world") + + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users = server.Handle("/users", server.Alice) assert.NotContains(users, "No posts.") assert.Contains(users, "Hello world") @@ -71,6 +89,8 @@ func TestFollow_DMUnfollowFollow(t *testing.T) { assert := assert.New(t) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "No posts.") assert.NotContains(users, "Hello @alice@localhost.localdomain:8443") @@ -81,6 +101,12 @@ func TestFollow_DMUnfollowFollow(t *testing.T) { dm := server.Handle("/users/dm?Hello%20%40alice%40localhost.localdomain%3a8443", server.Bob) assert.Regexp(`^30 /users/view/\S+\r\n$`, dm) + users = server.Handle("/users", server.Alice) + assert.Contains(users, "No posts.") + assert.NotContains(users, "Hello @alice@localhost.localdomain:8443") + + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users = server.Handle("/users", server.Alice) assert.NotContains(users, "No posts.") assert.Contains(users, "Hello @alice@localhost.localdomain:8443") @@ -88,6 +114,40 @@ func TestFollow_DMUnfollowFollow(t *testing.T) { unfollow := server.Handle("/users/unfollow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), unfollow) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + + users = server.Handle("/users", server.Alice) + assert.NotContains(users, "No posts.") + assert.Contains(users, "Hello @alice@localhost.localdomain:8443") +} + +func TestFollow_DMUnfollowBeforeFeedUpdate(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + + users := server.Handle("/users", server.Alice) + assert.Contains(users, "No posts.") + assert.NotContains(users, "Hello @alice@localhost.localdomain:8443") + + follow := server.Handle("/users/follow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) + assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), follow) + + dm := server.Handle("/users/dm?Hello%20%40alice%40localhost.localdomain%3a8443", server.Bob) + assert.Regexp(`^30 /users/view/\S+\r\n$`, dm) + + users = server.Handle("/users", server.Alice) + assert.Contains(users, "No posts.") + assert.NotContains(users, "Hello @alice@localhost.localdomain:8443") + + unfollow := server.Handle("/users/unfollow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) + assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), unfollow) + + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users = server.Handle("/users", server.Alice) assert.Contains(users, "No posts.") assert.NotContains(users, "Hello @alice@localhost.localdomain:8443") @@ -102,6 +162,8 @@ func TestFollow_PublicPost(t *testing.T) { follow := server.Handle("/users/follow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), follow) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "No posts.") assert.NotContains(users, "Hello world") @@ -109,6 +171,12 @@ func TestFollow_PublicPost(t *testing.T) { whisper := server.Handle("/users/say?Hello%20world", server.Bob) assert.Regexp(`^30 /users/view/\S+\r\n$`, whisper) + users = server.Handle("/users", server.Alice) + assert.Contains(users, "No posts.") + assert.NotContains(users, "Hello world") + + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users = server.Handle("/users", server.Alice) assert.NotContains(users, "No posts.") assert.Contains(users, "Hello world") @@ -123,6 +191,8 @@ func TestFollow_Mutual(t *testing.T) { follow := server.Handle("/users/follow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), follow) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "No posts.") assert.NotContains(users, "Hello world") @@ -139,6 +209,16 @@ func TestFollow_Mutual(t *testing.T) { reply := server.Handle(fmt.Sprintf("/users/reply/%s?Hello%%20Alice", id), server.Bob) assert.Regexp(`^30 /users/view/\S+\r\n$`, reply) + users = server.Handle("/users", server.Alice) + assert.Contains(users, "No posts.") + assert.NotContains(users, "Hello world") + + users = server.Handle("/users", server.Bob) + assert.Contains(users, "No posts.") + assert.NotContains(users, "Hello world") + + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users = server.Handle("/users", server.Alice) assert.NotContains(users, "No posts.") assert.Contains(users, "Hello Alice") @@ -150,6 +230,8 @@ func TestFollow_Mutual(t *testing.T) { follow = server.Handle("/users/follow/"+strings.TrimPrefix(server.Alice.ID, "https://"), server.Bob) assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Alice.ID, "https://")), follow) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users = server.Handle("/users", server.Bob) assert.NotContains(users, "No posts.") assert.Contains(users, "Hello world") diff --git a/test/reply_test.go b/test/reply_test.go index 3948e533..9c142233 100644 --- a/test/reply_test.go +++ b/test/reply_test.go @@ -17,7 +17,9 @@ limitations under the License. package test import ( + "context" "fmt" + "github.com/dimkr/tootik/inbox" "github.com/stretchr/testify/assert" "strings" "testing" @@ -45,6 +47,8 @@ func TestReply_AuthorNotFollowed(t *testing.T) { assert.Contains(view, "Hello world") assert.Contains(view, "Welcome Bob") + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Bob) assert.Contains(users, "Welcome Bob") @@ -78,6 +82,8 @@ func TestReply_AuthorFollowed(t *testing.T) { assert.Contains(view, "Hello world") assert.Contains(view, "Welcome Bob") + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Bob) assert.Contains(users, "Welcome Bob") @@ -111,6 +117,8 @@ func TestReply_PostToFollowers(t *testing.T) { assert.Contains(view, "Hello world") assert.Contains(view, "Welcome Bob") + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Bob) assert.Contains(users, "Welcome Bob") @@ -140,6 +148,8 @@ func TestReply_PostToFollowersNotFollowing(t *testing.T) { view = server.Handle("/users/view/"+id, server.Alice) assert.Equal("40 Post not found\r\n", view) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Bob) assert.NotContains(users, "Welcome Bob") @@ -176,6 +186,8 @@ func TestReply_PostToFollowersUnfollowedBeforeReply(t *testing.T) { assert.NotContains(view, "Hello world") assert.NotContains(view, "Welcome Bob") + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Bob) assert.NotContains(users, "Welcome Bob") @@ -211,6 +223,8 @@ func TestReply_PostToFollowersUnfollowedAfterReply(t *testing.T) { view = server.Handle("/users/view/"+id, server.Alice) assert.Equal("40 Post not found\r\n", view) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Bob) assert.Contains(users, "Welcome Bob") @@ -247,6 +261,8 @@ func TestReply_SelfReply(t *testing.T) { assert.Contains(view, "Hello world") assert.Contains(view, "Welcome me") + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Bob) assert.NotContains(users, "Welcome me") @@ -280,6 +296,8 @@ func TestReply_ReplyToPublicPostByFollowedUser(t *testing.T) { assert.Contains(view, "Hello world") assert.Contains(view, "Welcome Bob") + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "Hello world") assert.NotContains(users, "Welcome Bob") @@ -311,6 +329,8 @@ func TestReply_ReplyToPublicPostByNotFollowedUser(t *testing.T) { assert.Contains(view, "Hello world") assert.Contains(view, "Welcome Bob") + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.NotContains(users, "Hello world") assert.NotContains(users, "Welcome Bob") @@ -332,6 +352,8 @@ func TestReply_DM(t *testing.T) { dm := server.Handle("/users/dm?Hello%20%40alice%40localhost.localdomain%3a8443", server.Bob) assert.Regexp(`^30 /users/view/\S+\r\n$`, dm) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "Hello @alice@localhost.localdomain:8443") assert.NotContains(users, "Hello Bob") @@ -348,6 +370,8 @@ func TestReply_DM(t *testing.T) { reply := server.Handle(fmt.Sprintf("/users/reply/%s?Hello%%20Bob", id), server.Alice) assert.Regexp(`^30 /users/view/\S+\r\n$`, reply) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users = server.Handle("/users", server.Alice) assert.Contains(users, "Hello @alice@localhost.localdomain:8443") assert.NotContains(users, "Hello Bob") @@ -369,6 +393,8 @@ func TestReply_DMUnfollowed(t *testing.T) { dm := server.Handle("/users/dm?Hello%20%40alice%40localhost.localdomain%3a8443", server.Bob) assert.Regexp(`^30 /users/view/\S+\r\n$`, dm) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "Hello @alice@localhost.localdomain:8443") assert.NotContains(users, "Hello Bob") @@ -388,6 +414,50 @@ func TestReply_DMUnfollowed(t *testing.T) { reply := server.Handle(fmt.Sprintf("/users/reply/%s?Hello%%20Bob", id), server.Alice) assert.Regexp(`^30 /users/view/\S+\r\n$`, reply) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + + users = server.Handle("/users", server.Alice) + assert.Contains(users, "Hello @alice@localhost.localdomain:8443") + assert.NotContains(users, "Hello Bob") + + users = server.Handle("/users", server.Bob) + assert.NotContains(users, "Hello @alice@localhost.localdomain:8443") + assert.Contains(users, "Hello Bob") +} + +func TestReply_DMUnfollowedBeforeFeedUpdate(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + follow := server.Handle("/users/follow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) + assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), follow) + + dm := server.Handle("/users/dm?Hello%20%40alice%40localhost.localdomain%3a8443", server.Bob) + assert.Regexp(`^30 /users/view/\S+\r\n$`, dm) + + users := server.Handle("/users", server.Alice) + assert.NotContains(users, "Hello @alice@localhost.localdomain:8443") + assert.NotContains(users, "Hello Bob") + + users = server.Handle("/users", server.Bob) + assert.NotContains(users, "Hello @alice@localhost.localdomain:8443") + assert.NotContains(users, "Hello Bob") + + id := dm[15 : len(dm)-2] + + view := server.Handle("/users/view/"+id, server.Alice) + assert.Contains(view, "Hello @alice@localhost.localdomain:8443") + + unfollow := server.Handle("/users/unfollow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) + assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), unfollow) + + reply := server.Handle(fmt.Sprintf("/users/reply/%s?Hello%%20Bob", id), server.Alice) + assert.Regexp(`^30 /users/view/\S+\r\n$`, reply) + + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users = server.Handle("/users", server.Alice) assert.NotContains(users, "Hello @alice@localhost.localdomain:8443") assert.NotContains(users, "Hello Bob") @@ -409,6 +479,8 @@ func TestReply_DMToAnotherUser(t *testing.T) { dm := server.Handle("/users/dm?Hello%20%40alice%40localhost.localdomain%3a8443", server.Bob) assert.Regexp(`^30 /users/view/\S+\r\n$`, dm) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "Hello @alice@localhost.localdomain:8443") assert.NotContains(users, "Hello Bob") @@ -425,6 +497,8 @@ func TestReply_DMToAnotherUser(t *testing.T) { reply := server.Handle(fmt.Sprintf("/users/reply/%s?Hello%%20Bob", id), server.Carol) assert.Equal("40 Post not found\r\n", reply) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users = server.Handle("/users", server.Alice) assert.Contains(users, "Hello @alice@localhost.localdomain:8443") assert.NotContains(users, "Hello Bob") diff --git a/test/unfollow_test.go b/test/unfollow_test.go index 3a16e249..b8d89c25 100644 --- a/test/unfollow_test.go +++ b/test/unfollow_test.go @@ -17,7 +17,9 @@ limitations under the License. package test import ( + "context" "fmt" + "github.com/dimkr/tootik/inbox" "github.com/stretchr/testify/assert" "strings" "testing" @@ -35,12 +37,40 @@ func TestUnfollow_HappyFlow(t *testing.T) { say := server.Handle("/users/whisper?Hello%20followers", server.Bob) assert.Regexp(`^30 /users/view/\S+\r\n$`, say) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "Hello followers") unfollow := server.Handle("/users/unfollow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), unfollow) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + + users = server.Handle("/users", server.Alice) + assert.Contains(users, "Hello followers") +} + +func TestUnfollow_HappyFlowBeforeFeedUpdate(t *testing.T) { + server := newTestServer() + defer server.Shutdown() + + assert := assert.New(t) + + follow := server.Handle("/users/follow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) + assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), follow) + + say := server.Handle("/users/whisper?Hello%20followers", server.Bob) + assert.Regexp(`^30 /users/view/\S+\r\n$`, say) + + users := server.Handle("/users", server.Alice) + assert.NotContains(users, "Hello followers") + + unfollow := server.Handle("/users/unfollow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) + assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), unfollow) + + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users = server.Handle("/users", server.Alice) assert.NotContains(users, "Hello followers") } @@ -57,18 +87,24 @@ func TestUnfollow_FollowAgain(t *testing.T) { say := server.Handle("/users/whisper?Hello%20followers", server.Bob) assert.Regexp(`^30 /users/view/\S+\r\n$`, say) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "Hello followers") unfollow := server.Handle("/users/unfollow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), unfollow) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users = server.Handle("/users", server.Alice) - assert.NotContains(users, "Hello followers") + assert.Contains(users, "Hello followers") follow = server.Handle("/users/follow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), follow) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users = server.Handle("/users", server.Alice) assert.Contains(users, "Hello followers") } diff --git a/test/upload_edit_test.go b/test/upload_edit_test.go index 455589d3..b6bdc4af 100644 --- a/test/upload_edit_test.go +++ b/test/upload_edit_test.go @@ -17,7 +17,9 @@ limitations under the License. package test import ( + "context" "fmt" + "github.com/dimkr/tootik/inbox" "github.com/stretchr/testify/assert" "strings" "testing" @@ -33,6 +35,8 @@ func TestUploadEdit_HappyFlow(t *testing.T) { follow := server.Handle("/users/follow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), follow) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "No posts.") assert.NotContains(users, "Hello followers") @@ -48,6 +52,8 @@ func TestUploadEdit_HappyFlow(t *testing.T) { edit := server.Upload(fmt.Sprintf("/users/upload/edit/%s;mime=text/plain;size=15", id), server.Bob, []byte("Hello followers")) assert.Equal(fmt.Sprintf("30 gemini://%s/users/view/%s\r\n", domain, id), edit) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users = server.Handle("/users", server.Alice) assert.NotContains(users, "No posts.") assert.Contains(users, "Hello followers") @@ -55,9 +61,7 @@ func TestUploadEdit_HappyFlow(t *testing.T) { edit = server.Upload(fmt.Sprintf("/users/upload/edit/%s;mime=text/plain;size=16", id), server.Bob, []byte("Hello, followers")) assert.Equal("40 Please try again later\r\n", edit) - users = server.Handle("/users", server.Alice) - assert.NotContains(users, "No posts.") - assert.Contains(users, "Hello followers") + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) users = server.Handle("/users", server.Alice) assert.NotContains(users, "No posts.") @@ -73,6 +77,8 @@ func TestUploadEdit_Empty(t *testing.T) { follow := server.Handle("/users/follow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), follow) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "No posts.") assert.NotContains(users, "Hello followers") @@ -98,6 +104,8 @@ func TestUploadEdit_SizeLimit(t *testing.T) { follow := server.Handle("/users/follow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), follow) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "No posts.") assert.NotContains(users, "Hello followers") @@ -125,6 +133,8 @@ func TestUploadEdit_InvalidSize(t *testing.T) { follow := server.Handle("/users/follow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), follow) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "No posts.") assert.NotContains(users, "Hello followers") @@ -150,6 +160,8 @@ func TestUploadEdit_InvalidType(t *testing.T) { follow := server.Handle("/users/follow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), follow) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "No posts.") assert.NotContains(users, "Hello followers") @@ -175,6 +187,8 @@ func TestUploadEdit_NoSize(t *testing.T) { follow := server.Handle("/users/follow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), follow) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "No posts.") assert.NotContains(users, "Hello followers") @@ -200,6 +214,8 @@ func TestUploadEdit_NoType(t *testing.T) { follow := server.Handle("/users/follow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), follow) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "No posts.") assert.NotContains(users, "Hello followers") diff --git a/test/upload_reply_test.go b/test/upload_reply_test.go index 0a91b662..8c3bab43 100644 --- a/test/upload_reply_test.go +++ b/test/upload_reply_test.go @@ -17,7 +17,9 @@ limitations under the License. package test import ( + "context" "fmt" + "github.com/dimkr/tootik/inbox" "github.com/stretchr/testify/assert" "strings" "testing" @@ -48,6 +50,8 @@ func TestUploadReply_PostToFollowers(t *testing.T) { assert.Contains(view, "Hello world") assert.Contains(view, "Welcome Bob") + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Bob) assert.Contains(users, "Welcome Bob") diff --git a/test/users_test.go b/test/users_test.go index e93c152c..603ed00c 100644 --- a/test/users_test.go +++ b/test/users_test.go @@ -35,6 +35,8 @@ func TestUsers_NoPosts(t *testing.T) { assert := assert.New(t) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "No posts.") } @@ -45,6 +47,8 @@ func TestUsers_UnauthenticatedUser(t *testing.T) { assert := assert.New(t) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", nil) assert.Equal("30 /oops\r\n", users) } @@ -61,6 +65,8 @@ func TestUsers_DM(t *testing.T) { dm := server.Handle("/users/dm?Hello%20%40alice", server.Bob) assert.Regexp(`^30 /users/view/\S+\r\n$`, dm) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "Hello @alice") } @@ -80,6 +86,8 @@ func TestUsers_DMNotFollowing(t *testing.T) { unfollow := server.Handle("/users/unfollow/"+strings.TrimPrefix(server.Bob.ID, "https://"), server.Alice) assert.Equal(fmt.Sprintf("30 /users/outbox/%s\r\n", strings.TrimPrefix(server.Bob.ID, "https://")), unfollow) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.NotContains(users, "Hello @alice") } @@ -96,6 +104,8 @@ func TestUsers_PostToFollowers(t *testing.T) { whisper := server.Handle("/users/whisper?Hello%20world", server.Bob) assert.Regexp(`^30 /users/view/\S+\r\n$`, whisper) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "Hello world") } @@ -109,6 +119,8 @@ func TestUsers_PostToFollowersNotFollowing(t *testing.T) { whisper := server.Handle("/users/whisper?Hello%20world", server.Bob) assert.Regexp(`^30 /users/view/\S+\r\n$`, whisper) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.NotContains(users, "Hello world") } @@ -125,6 +137,8 @@ func TestUsers_PublicPost(t *testing.T) { say := server.Handle("/users/say?Hello%20world", server.Bob) assert.Regexp(`^30 /users/view/\S+\r\n$`, say) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "Hello world") } @@ -138,6 +152,8 @@ func TestUsers_PublicPostNotFollowing(t *testing.T) { say := server.Handle("/users/say?Hello%20world", server.Bob) assert.Regexp(`^30 /users/view/\S+\r\n$`, say) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.NotContains(users, "Hello world") } @@ -188,6 +204,8 @@ func TestUsers_PublicPostShared(t *testing.T) { assert.NoError(err) assert.Equal(1, n) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.Contains(users, "Hello world") } @@ -241,6 +259,8 @@ func TestUsers_PublicPostSharedNotFollowing(t *testing.T) { unfollow := server.Handle("/users/unfollow/127.0.0.1/user/erin", server.Alice) assert.Equal("30 /users/outbox/127.0.0.1/user/erin\r\n", unfollow) + assert.NoError((inbox.FeedUpdater{Domain: domain, Config: server.cfg, DB: server.db}).Run(context.Background())) + users := server.Handle("/users", server.Alice) assert.NotContains(users, "Hello world") }