From 77ad6e78b5937652fe6f37be7f83e058e9dd3cfa Mon Sep 17 00:00:00 2001 From: Dima Krasner Date: Thu, 10 Oct 2024 08:32:02 +0300 Subject: [PATCH 1/6] implement user suspend --- cmd/tootik/main.go | 9 ++- outbox/forward.go | 2 +- outbox/suspend.go | 143 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 outbox/suspend.go diff --git a/cmd/tootik/main.go b/cmd/tootik/main.go index b8b51e45..1dadee36 100644 --- a/cmd/tootik/main.go +++ b/cmd/tootik/main.go @@ -87,6 +87,7 @@ func main() { fmt.Fprintf(flag.CommandLine.Output(), "\n%s [flag]... add-community NAME\n\tAdd a community\n", os.Args[0]) fmt.Fprintf(flag.CommandLine.Output(), "\n%s [flag]... set-bio NAME PATH\n\tSet user's bio\n", os.Args[0]) fmt.Fprintf(flag.CommandLine.Output(), "\n%s [flag]... set-avatar NAME PATH\n\tSet user's avatar\n", os.Args[0]) + fmt.Fprintf(flag.CommandLine.Output(), "\n%s [flag]... suspend NAME\n\tSuspend user\n", os.Args[0]) os.Exit(2) } @@ -98,7 +99,7 @@ func main() { } cmd := flag.Arg(0) - if !((cmd == "" && flag.NArg() == 0) || (cmd == "add-community" && flag.NArg() == 2 && flag.Arg(1) != "") || ((cmd == "set-bio" || cmd == "set-avatar") && flag.NArg() == 3 && flag.Arg(1) != "" && flag.Arg(2) != "")) { + if !((cmd == "" && flag.NArg() == 0) || ((cmd == "add-community" || cmd == "suspend") && flag.NArg() == 2 && flag.Arg(1) != "") || ((cmd == "set-bio" || cmd == "set-avatar") && flag.NArg() == 3 && flag.Arg(1) != "" && flag.Arg(2) != "")) { flag.Usage() } @@ -308,6 +309,12 @@ func main() { } return + + case "suspend": + if err := outbox.Suspend(ctx, *domain, flag.Arg(1), &cfg, db); err != nil { + panic(err) + } + return } handler, err := front.NewHandler(*domain, *closed, &cfg, resolver, db) diff --git a/outbox/forward.go b/outbox/forward.go index cd3a9901..facedb87 100644 --- a/outbox/forward.go +++ b/outbox/forward.go @@ -164,7 +164,7 @@ func ForwardActivity[T ap.RawActivity](ctx context.Context, domain string, cfg * } var shouldForward int - if err := tx.QueryRowContext(ctx, `select exists (select 1 from notes join persons on persons.id = notes.author and (notes.public = 1 or exists (select 1 from json_each(notes.object->'$.to') where value = persons.actor->>'$.followers') or exists (select 1 from json_each(notes.object->'$.cc') where value = persons.actor->>'$.followers')) where notes.id = ?)`, firstPostID).Scan(&shouldForward); err != nil { + if err := tx.QueryRowContext(ctx, `select exists (select 1 from notes join persons on persons.id = notes.author and (notes.public = 1 or exists (select 1 from json_each(notes.object->'$.to') where value = persons.actor->>'$.followers') or exists (select 1 from json_each(notes.object->'$.cc') where value = persons.actor->>'$.followers')) where notes.id = ? and notes.author != ?)`, firstPostID, activity.Actor).Scan(&shouldForward); err != nil { return err } if shouldForward == 0 { diff --git a/outbox/suspend.go b/outbox/suspend.go new file mode 100644 index 00000000..d2176726 --- /dev/null +++ b/outbox/suspend.go @@ -0,0 +1,143 @@ +/* +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 outbox + +import ( + "context" + "database/sql" + "fmt" + "github.com/dimkr/tootik/ap" + "github.com/dimkr/tootik/cfg" + "time" +) + +func undoFollows(ctx context.Context, domain, actorID string, db *sql.DB) error { + follows, err := db.QueryContext(ctx, `select id, followed from follows where follower = ?`, actorID) + if err != nil { + return err + } + defer follows.Close() + + for follows.Next() { + var followID, followed string + if err := follows.Scan(&followID, &followed); err != nil { + return err + } + + if err := Unfollow(ctx, domain, db, actorID, followed, followID); err != nil { + return err + } + } + + return nil +} + +func undoShares(ctx context.Context, domain, actorID string, db *sql.DB) error { + shares, err := db.QueryContext(ctx, `select activity from outbox where activity->>'$.actor' = $1 and sender = $1 and activity->>'$.type' = 'Announce'`, actorID) + if err != nil { + return err + } + defer shares.Close() + + for shares.Next() { + var share ap.Activity + if err := shares.Scan(&share); err != nil { + return err + } + + if err := Undo(ctx, domain, db, &share); err != nil { + return err + } + } + + return nil +} + +func deletePosts(ctx context.Context, domain, actorID string, cfg *cfg.Config, db *sql.DB) error { + posts, err := db.QueryContext(ctx, `select object from notes where author = ?`, actorID) + if err != nil { + return err + } + defer posts.Close() + + for posts.Next() { + var post ap.Object + if err := posts.Scan(&post); err != nil { + return err + } + + if err := Delete(ctx, domain, cfg, db, &post); err != nil { + return err + } + } + + return nil +} + +func Suspend(ctx context.Context, domain, user string, cfg *cfg.Config, db *sql.DB) error { + actorID := fmt.Sprintf("https://%s/user/%s", domain, user) + + var actor ap.Actor + if err := db.QueryRowContext(ctx, `select actor from persons where id = ?`, actorID).Scan(&actor); err != nil { + return err + } + + now := time.Now() + + // clear display name, summary and avatar + actor.Name = "" + actor.Summary = "Suspended " + now.Format(time.DateOnly) + actor.Icon = nil + + // mark as suspended + actor.Suspended = true + + actor.Updated = &ap.Time{Time: now} + + // deny access + if _, err := db.ExecContext(ctx, `update persons set privkey = null, actor = ? where id = ?`, &actor, actorID); err != nil { + return err + } + + tx, err := db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + if err := UpdateActor(ctx, domain, tx, actorID); err != nil { + return err + } + + if err := tx.Commit(); err != nil { + return err + } + + if err := undoShares(ctx, domain, actorID, db); err != nil { + return err + } + + if err := deletePosts(ctx, domain, actorID, cfg, db); err != nil { + return err + } + + if _, err := db.ExecContext(ctx, `delete from bookmarks where by = ?`, actorID); err != nil { + return err + } + + return nil +} From fe2f625c56e156f65407a8c4e49c61634068ded8 Mon Sep 17 00:00:00 2001 From: Dima Krasner Date: Thu, 10 Oct 2024 08:34:55 +0300 Subject: [PATCH 2/6] oops --- outbox/suspend.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/outbox/suspend.go b/outbox/suspend.go index d2176726..fe0e9c7a 100644 --- a/outbox/suspend.go +++ b/outbox/suspend.go @@ -127,6 +127,10 @@ func Suspend(ctx context.Context, domain, user string, cfg *cfg.Config, db *sql. return err } + if err := undoFollows(ctx, domain, actorID, db); err != nil { + return err + } + if err := undoShares(ctx, domain, actorID, db); err != nil { return err } From 8d318e999d87e3cfc68d1b5906b28920abb015a5 Mon Sep 17 00:00:00 2001 From: Dima Krasner Date: Fri, 11 Oct 2024 12:25:32 +0300 Subject: [PATCH 3/6] cleanup --- outbox/forward.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/outbox/forward.go b/outbox/forward.go index facedb87..2e35225a 100644 --- a/outbox/forward.go +++ b/outbox/forward.go @@ -164,7 +164,7 @@ func ForwardActivity[T ap.RawActivity](ctx context.Context, domain string, cfg * } var shouldForward int - if err := tx.QueryRowContext(ctx, `select exists (select 1 from notes join persons on persons.id = notes.author and (notes.public = 1 or exists (select 1 from json_each(notes.object->'$.to') where value = persons.actor->>'$.followers') or exists (select 1 from json_each(notes.object->'$.cc') where value = persons.actor->>'$.followers')) where notes.id = ? and notes.author != ?)`, firstPostID, activity.Actor).Scan(&shouldForward); err != nil { + if err := tx.QueryRowContext(ctx, `select exists (select 1 from notes join persons on persons.id = notes.author and (notes.public = 1 or exists (select 1 from json_each(notes.object->'$.to') where value = persons.actor->>'$.followers') or exists (select 1 from json_each(notes.object->'$.cc') where value = persons.actor->>'$.followers')) where notes.id = ?)`, firstPostID, activity.Actor).Scan(&shouldForward); err != nil { return err } if shouldForward == 0 { From 9598302dc3f896408c325dd203a678f76aa8a56b Mon Sep 17 00:00:00 2001 From: Dima Krasner Date: Fri, 11 Oct 2024 12:25:55 +0300 Subject: [PATCH 4/6] cleanup --- outbox/forward.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/outbox/forward.go b/outbox/forward.go index 2e35225a..cd3a9901 100644 --- a/outbox/forward.go +++ b/outbox/forward.go @@ -164,7 +164,7 @@ func ForwardActivity[T ap.RawActivity](ctx context.Context, domain string, cfg * } var shouldForward int - if err := tx.QueryRowContext(ctx, `select exists (select 1 from notes join persons on persons.id = notes.author and (notes.public = 1 or exists (select 1 from json_each(notes.object->'$.to') where value = persons.actor->>'$.followers') or exists (select 1 from json_each(notes.object->'$.cc') where value = persons.actor->>'$.followers')) where notes.id = ?)`, firstPostID, activity.Actor).Scan(&shouldForward); err != nil { + if err := tx.QueryRowContext(ctx, `select exists (select 1 from notes join persons on persons.id = notes.author and (notes.public = 1 or exists (select 1 from json_each(notes.object->'$.to') where value = persons.actor->>'$.followers') or exists (select 1 from json_each(notes.object->'$.cc') where value = persons.actor->>'$.followers')) where notes.id = ?)`, firstPostID).Scan(&shouldForward); err != nil { return err } if shouldForward == 0 { From fd74c00a0a425db77c84b20974cfe5bbe2a3196e Mon Sep 17 00:00:00 2001 From: Dima Krasner Date: Fri, 11 Oct 2024 12:27:26 +0300 Subject: [PATCH 5/6] cleanup --- outbox/suspend.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/outbox/suspend.go b/outbox/suspend.go index fe0e9c7a..778db0e7 100644 --- a/outbox/suspend.go +++ b/outbox/suspend.go @@ -26,7 +26,7 @@ import ( ) func undoFollows(ctx context.Context, domain, actorID string, db *sql.DB) error { - follows, err := db.QueryContext(ctx, `select id, followed from follows where follower = ?`, actorID) + follows, err := db.QueryContext(ctx, `select id, followed from follows where follower = ? order by inserted`, actorID) if err != nil { return err } @@ -47,7 +47,7 @@ func undoFollows(ctx context.Context, domain, actorID string, db *sql.DB) error } func undoShares(ctx context.Context, domain, actorID string, db *sql.DB) error { - shares, err := db.QueryContext(ctx, `select activity from outbox where activity->>'$.actor' = $1 and sender = $1 and activity->>'$.type' = 'Announce'`, actorID) + shares, err := db.QueryContext(ctx, `select activity from outbox where activity->>'$.actor' = $1 and sender = $1 and activity->>'$.type' = 'Announce' order by inserted`, actorID) if err != nil { return err } @@ -68,7 +68,7 @@ func undoShares(ctx context.Context, domain, actorID string, db *sql.DB) error { } func deletePosts(ctx context.Context, domain, actorID string, cfg *cfg.Config, db *sql.DB) error { - posts, err := db.QueryContext(ctx, `select object from notes where author = ?`, actorID) + posts, err := db.QueryContext(ctx, `select object from notes where author = ? order by inserted`, actorID) if err != nil { return err } From 4586918c58156ff39bb110346039cd824f81f804 Mon Sep 17 00:00:00 2001 From: Dima Krasner Date: Thu, 17 Oct 2024 16:18:27 +0300 Subject: [PATCH 6/6] oops --- outbox/suspend.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/outbox/suspend.go b/outbox/suspend.go index 778db0e7..e07e8761 100644 --- a/outbox/suspend.go +++ b/outbox/suspend.go @@ -139,6 +139,22 @@ func Suspend(ctx context.Context, domain, user string, cfg *cfg.Config, db *sql. return err } + if _, err := db.ExecContext(ctx, `delete from feed where follower = ?`, actorID); err != nil { + return err + } + + if _, err := db.ExecContext(ctx, `delete from feed where followed = ?`, actorID); err != nil { + return err + } + + if _, err := db.ExecContext(ctx, `delete from feed where author->>'$.id' = ?`, actorID); err != nil { + return err + } + + if _, err := db.ExecContext(ctx, `update feed set sharer = null where sharer->>'$.id' = ?`, actorID); err != nil { + return err + } + if _, err := db.ExecContext(ctx, `delete from bookmarks where by = ?`, actorID); err != nil { return err }