diff options
Diffstat (limited to 'route')
-rw-r--r-- | route/routes/actor.go | 680 | ||||
-rw-r--r-- | route/routes/admin.go | 291 | ||||
-rw-r--r-- | route/routes/api.go | 55 | ||||
-rw-r--r-- | route/routes/boardmgmt.go | 47 | ||||
-rw-r--r-- | route/routes/main.go | 100 | ||||
-rw-r--r-- | route/routes/news.go | 73 | ||||
-rw-r--r-- | route/routes/webfinger.go | 58 | ||||
-rw-r--r-- | route/structs.go | 56 | ||||
-rw-r--r-- | route/util.go | 377 |
9 files changed, 1737 insertions, 0 deletions
diff --git a/route/routes/actor.go b/route/routes/actor.go new file mode 100644 index 0000000..e27133b --- /dev/null +++ b/route/routes/actor.go @@ -0,0 +1,680 @@ +package routes + +import ( + "bytes" + "errors" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "regexp" + "strconv" + + "github.com/FChannel0/FChannel-Server/activitypub" + "github.com/FChannel0/FChannel-Server/config" + "github.com/FChannel0/FChannel-Server/post" + "github.com/FChannel0/FChannel-Server/route" + "github.com/FChannel0/FChannel-Server/util" + "github.com/FChannel0/FChannel-Server/webfinger" + "github.com/gofiber/fiber/v2" +) + +func ActorInbox(ctx *fiber.Ctx) error { + activity, err := activitypub.GetActivityFromJson(ctx) + if err != nil { + return util.MakeError(err, "ActorInbox") + } + + if activity.Actor.PublicKey.Id == "" { + nActor, err := activitypub.FingerActor(activity.Actor.Id) + if err != nil { + return util.MakeError(err, "ActorInbox") + } + + activity.Actor = &nActor + } + + if !activity.Actor.VerifyHeaderSignature(ctx) { + response := activity.Reject() + return response.MakeRequestInbox() + } + + switch activity.Type { + case "Create": + for _, e := range activity.To { + actor := activitypub.Actor{Id: e} + if res, err := actor.IsLocal(); err == nil && res { + if res, err := activity.Actor.IsLocal(); err == nil && res { + reqActivity := activitypub.Activity{Id: activity.Object.Id} + col, err := reqActivity.GetCollection() + if err != nil { + return util.MakeError(err, "ActorInbox") + } + + if len(col.OrderedItems) < 1 { + break + } + + if err := activity.Object.WriteCache(); err != nil { + return util.MakeError(err, "ActorInbox") + } + + actor, err := activitypub.GetActorFromDB(e) + if err != nil { + return util.MakeError(err, "ActorInbox") + } + + if err := actor.ArchivePosts(); err != nil { + return util.MakeError(err, "ActorInbox") + } + + //SendToFollowers(e, activity) + } else if err != nil { + return util.MakeError(err, "ActorInbox") + } + } else if err != nil { + return util.MakeError(err, "ActorInbox") + } + } + + break + + case "Delete": + for _, e := range activity.To { + actor, err := activitypub.GetActorFromDB(e) + if err != nil { + return util.MakeError(err, "") + } + + if actor.Id != "" && actor.Id != config.Domain { + if activity.Object.Replies.OrderedItems != nil { + for _, k := range activity.Object.Replies.OrderedItems { + if err := k.Tombstone(); err != nil { + return util.MakeError(err, "ActorInbox") + } + } + } + + if err := activity.Object.Tombstone(); err != nil { + return util.MakeError(err, "ActorInbox") + } + if err := actor.UnArchiveLast(); err != nil { + return util.MakeError(err, "ActorInbox") + } + break + } + } + break + + case "Follow": + for _, e := range activity.To { + if _, err := activitypub.GetActorFromDB(e); err == nil { + response := activity.AcceptFollow() + response, err := response.SetActorFollower() + + if err != nil { + return util.MakeError(err, "ActorInbox") + } + + if err := response.MakeRequestInbox(); err != nil { + return util.MakeError(err, "ActorInbox") + } + + alreadyFollowing, err := response.Actor.IsAlreadyFollowing(response.Object.Id) + + if err != nil { + return util.MakeError(err, "ActorInbox") + } + + objActor, err := activitypub.FingerActor(response.Object.Actor) + + if err != nil || objActor.Id == "" { + return util.MakeError(err, "ActorInbox") + } + + reqActivity := activitypub.Activity{Id: objActor.Following} + remoteActorFollowingCol, err := reqActivity.GetCollection() + + if err != nil { + return util.MakeError(err, "ActorInbox") + } + + alreadyFollow := false + + for _, e := range remoteActorFollowingCol.Items { + if e.Id == response.Actor.Id { + alreadyFollowing = true + } + } + + autoSub, err := response.Actor.GetAutoSubscribe() + + if err != nil { + return util.MakeError(err, "ActorInbox") + } + + if autoSub && !alreadyFollow && alreadyFollowing { + followActivity, err := response.Actor.MakeFollowActivity(response.Object.Actor) + + if err != nil { + return util.MakeError(err, "ActorInbox") + } + + if err := followActivity.MakeRequestOutbox(); err != nil { + return util.MakeError(err, "ActorInbox") + } + } + } else if err != nil { + return util.MakeError(err, "ActorInbox") + } else { + config.Log.Println("follow request for rejected") + response := activity.Reject() + return response.MakeRequestInbox() + } + } + break + + case "Reject": + if activity.Object.Object.Type == "Follow" { + config.Log.Println("follow rejected") + if _, err := activity.SetActorFollowing(); err != nil { + return util.MakeError(err, "ActorInbox") + } + } + break + } + + return nil +} + +func ActorOutbox(ctx *fiber.Ctx) error { + //var activity activitypub.Activity + actor, err := webfinger.GetActorFromPath(ctx.Path(), "/") + if err != nil { + return util.MakeError(err, "ActorOutbox") + } + + if activitypub.AcceptActivity(ctx.Get("Accept")) { + actor.GetOutbox(ctx) + return nil + } + + return route.ParseOutboxRequest(ctx, actor) +} + +func ActorFollowing(ctx *fiber.Ctx) error { + actor, _ := activitypub.GetActorFromDB(config.Domain + "/" + ctx.Params("actor")) + return actor.GetFollowingResp(ctx) +} + +func ActorFollowers(ctx *fiber.Ctx) error { + actor, _ := activitypub.GetActorFromDB(config.Domain + "/" + ctx.Params("actor")) + return actor.GetFollowersResp(ctx) +} + +func ActorReported(c *fiber.Ctx) error { + // STUB + + return c.SendString("actor reported") +} + +func ActorArchive(c *fiber.Ctx) error { + // STUB + + return c.SendString("actor archive") +} + +func ActorPost(ctx *fiber.Ctx) error { + header, _ := ctx.FormFile("file") + + if ctx.FormValue("inReplyTo") == "" && header == nil { + return ctx.Render("403", fiber.Map{ + "message": "Media is required for new posts", + }) + } + + var file multipart.File + + if header != nil { + file, _ = header.Open() + } + + if file != nil && header.Size > (7<<20) { + return ctx.Render("403", fiber.Map{ + "message": "7MB max file size", + }) + } + + if is, _ := util.IsPostBlacklist(ctx.FormValue("comment")); is { + errors.New("\n\nBlacklist post blocked\n\n") + return ctx.Redirect("/", 301) + } + + if ctx.FormValue("inReplyTo") == "" || file == nil { + if ctx.FormValue("comment") == "" && ctx.FormValue("subject") == "" { + return ctx.Render("403", fiber.Map{ + "message": "Comment or Subject required", + }) + } + } + + if len(ctx.FormValue("comment")) > 2000 { + return ctx.Render("403", fiber.Map{ + "message": "Comment limit 2000 characters", + }) + } + + if len(ctx.FormValue("subject")) > 100 || len(ctx.FormValue("name")) > 100 || len(ctx.FormValue("options")) > 100 { + return ctx.Render("403", fiber.Map{ + "message": "Name, Subject or Options limit 100 characters", + }) + } + + if ctx.FormValue("captcha") == "" { + return ctx.Render("403", fiber.Map{ + "message": "Incorrect Captcha", + }) + } + + b := bytes.Buffer{} + we := multipart.NewWriter(&b) + + if file != nil { + var fw io.Writer + + fw, err := we.CreateFormFile("file", header.Filename) + + if err != nil { + errors.New("error with form file create") + } + _, err = io.Copy(fw, file) + + if err != nil { + errors.New("error with form file copy") + } + } + + reply, _ := post.ParseCommentForReply(ctx.FormValue("comment")) + + form, _ := ctx.MultipartForm() + + for key, r0 := range form.Value { + if key == "captcha" { + err := we.WriteField(key, ctx.FormValue("captchaCode")+":"+ctx.FormValue("captcha")) + if err != nil { + errors.New("error with writing captcha field") + } + } else if key == "name" { + name, tripcode, _ := post.CreateNameTripCode(ctx) + + err := we.WriteField(key, name) + if err != nil { + errors.New("error with writing name field") + } + + err = we.WriteField("tripcode", tripcode) + if err != nil { + errors.New("error with writing tripcode field") + } + } else { + err := we.WriteField(key, r0[0]) + if err != nil { + errors.New("error with writing field") + } + } + } + + if ctx.FormValue("inReplyTo") == "" && reply != "" { + err := we.WriteField("inReplyTo", reply) + if err != nil { + errors.New("error with writing inReplyTo field") + } + } + + we.Close() + + sendTo := ctx.FormValue("sendTo") + + req, err := http.NewRequest("POST", sendTo, &b) + + if err != nil { + errors.New("error with post form req") + } + + req.Header.Set("Content-Type", we.FormDataContentType()) + + resp, err := util.RouteProxy(req) + + if err != nil { + errors.New("error with post form resp") + } + + defer resp.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + + if resp.StatusCode == 200 { + + var obj activitypub.ObjectBase + + obj = post.ParseOptions(ctx, obj) + for _, e := range obj.Option { + if e == "noko" || e == "nokosage" { + return ctx.Redirect(config.Domain+"/"+ctx.FormValue("boardName")+"/"+util.ShortURL(ctx.FormValue("sendTo"), string(body)), 301) + } + } + + if ctx.FormValue("returnTo") == "catalog" { + return ctx.Redirect(config.Domain+"/"+ctx.FormValue("boardName")+"/catalog", 301) + } else { + return ctx.Redirect(config.Domain+"/"+ctx.FormValue("boardName"), 301) + } + } + + if resp.StatusCode == 403 { + return ctx.Render("403", fiber.Map{ + "message": string(body), + }) + } + + return ctx.Redirect(config.Domain+"/"+ctx.FormValue("boardName"), 301) +} + +func ActorPostGet(ctx *fiber.Ctx) error { + + actor, err := activitypub.GetActorByNameFromDB(ctx.Params("actor")) + if err != nil { + return nil + } + + // this is a activitpub json request return json instead of html page + if activitypub.AcceptActivity(ctx.Get("Accept")) { + route.GetActorPost(ctx, ctx.Path()) + return nil + } + + re := regexp.MustCompile("\\w+$") + postId := re.FindString(ctx.Path()) + + inReplyTo := actor.Id + "/" + postId + + var data route.PageData + + re = regexp.MustCompile("f(\\w|[!@#$%^&*<>])+-(\\w|[!@#$%^&*<>])+") + + if re.MatchString(ctx.Path()) { // if non local actor post + name := activitypub.GetActorFollowNameFromPath(ctx.Path()) + + followActors, err := actor.GetFollowFromName(name) + if err != nil { + return util.MakeError(err, "PostGet") + } + + followCollection, err := activitypub.GetActorsFollowPostFromId(followActors, postId) + if err != nil { + return util.MakeError(err, "PostGet") + } + + if len(followCollection.OrderedItems) > 0 { + data.Board.InReplyTo = followCollection.OrderedItems[0].Id + data.Posts = append(data.Posts, followCollection.OrderedItems[0]) + + actor, err := activitypub.FingerActor(data.Board.InReplyTo) + if err != nil { + return util.MakeError(err, "PostGet") + } + + data.Board.Post.Actor = actor.Id + } + } else { + obj := activitypub.ObjectBase{Id: inReplyTo} + collection, err := obj.GetCollectionFromPath() + if err != nil { + return util.MakeError(err, "PostGet") + } + + if collection.Actor.Id != "" { + data.Board.Post.Actor = collection.Actor.Id + data.Board.InReplyTo = inReplyTo + } + + if len(collection.OrderedItems) > 0 { + data.Posts = append(data.Posts, collection.OrderedItems[0]) + } + } + + if len(data.Posts) > 0 { + data.PostId = util.ShortURL(data.Board.To, data.Posts[0].Id) + } + + data.Board.Name = actor.Name + data.Board.PrefName = actor.PreferredUsername + data.Board.To = actor.Outbox + data.Board.Actor = actor + data.Board.Summary = actor.Summary + data.Board.ModCred, _ = util.GetPasswordFromSession(ctx) + data.Board.Domain = config.Domain + data.Board.Restricted = actor.Restricted + data.ReturnTo = "feed" + + capt, err := util.GetRandomCaptcha() + if err != nil { + return util.MakeError(err, "PostGet") + } + data.Board.Captcha = config.Domain + "/" + capt + data.Board.CaptchaCode = post.GetCaptchaCode(data.Board.Captcha) + + data.Instance, err = activitypub.GetActorFromDB(config.Domain) + if err != nil { + return util.MakeError(err, "PostGet") + } + + data.Key = config.Key + data.Boards = webfinger.Boards + + data.Title = "/" + data.Board.Name + "/ - " + data.PostId + + if len(data.Posts) > 0 { + data.Meta.Description = data.Posts[0].Content + data.Meta.Url = data.Posts[0].Id + data.Meta.Title = data.Posts[0].Name + data.Meta.Preview = data.Posts[0].Preview.Href + } + + data.Themes = &config.Themes + data.ThemeCookie = route.GetThemeCookie(ctx) + + return ctx.Render("npost", fiber.Map{ + "page": data, + }, "layouts/main") +} + +func ActorCatalogGet(ctx *fiber.Ctx) error { + actorName := ctx.Params("actor") + actor, err := activitypub.GetActorByNameFromDB(actorName) + if err != nil { + return util.MakeError(err, "CatalogGet") + } + + collection, err := actor.GetCatalogCollection() + + // TODO: implement this in template functions + // "showArchive": func() bool { + // col, err := db.GetActorCollectionDBTypeLimit(collection.Actor.Id, "Archive", 1) + // if err != nil { + // // TODO: figure out what to do here + // panic(err) + // } + // + // if len(col.OrderedItems) > 0 { + // return true + // } + // return false + //}, + + var data route.PageData + data.Board.Name = actor.Name + data.Board.PrefName = actor.PreferredUsername + data.Board.InReplyTo = "" + data.Board.To = actor.Outbox + data.Board.Actor = actor + data.Board.Summary = actor.Summary + data.Board.ModCred, _ = util.GetPasswordFromSession(ctx) + data.Board.Domain = config.Domain + data.Board.Restricted = actor.Restricted + data.Key = config.Key + data.ReturnTo = "catalog" + + data.Board.Post.Actor = actor.Id + + data.Instance, err = activitypub.GetActorFromDB(config.Domain) + if err != nil { + return util.MakeError(err, "CatalogGet") + } + + capt, err := util.GetRandomCaptcha() + if err != nil { + return util.MakeError(err, "CatalogGet") + } + + data.Board.Captcha = config.Domain + "/" + capt + data.Board.CaptchaCode = post.GetCaptchaCode(data.Board.Captcha) + + data.Title = "/" + data.Board.Name + "/ - catalog" + + data.Boards = webfinger.Boards + data.Posts = collection.OrderedItems + + data.Meta.Description = data.Board.Summary + data.Meta.Url = data.Board.Actor.Id + data.Meta.Title = data.Title + + data.Themes = &config.Themes + data.ThemeCookie = route.GetThemeCookie(ctx) + + return ctx.Render("catalog", fiber.Map{ + "page": data, + }, "layouts/main") +} + +func ActorOutboxGet(ctx *fiber.Ctx) error { + actor, err := activitypub.GetActorByNameFromDB(ctx.Params("actor")) + + if err != nil { + return nil + } + + if activitypub.AcceptActivity(ctx.Get("Accept")) { + actor.GetInfoResp(ctx) + return nil + } + + var page int + if postNum := ctx.Query("page"); postNum != "" { + if page, err = strconv.Atoi(postNum); err != nil { + return util.MakeError(err, "OutboxGet") + } + } + + collection, err := actor.WantToServePage(page) + if err != nil { + return util.MakeError(err, "OutboxGet") + } + + var offset = 15 + var pages []int + pageLimit := (float64(collection.TotalItems) / float64(offset)) + + if pageLimit > 11 { + pageLimit = 11 + } + + for i := 0.0; i < pageLimit; i++ { + pages = append(pages, int(i)) + } + + var data route.PageData + data.Board.Name = actor.Name + data.Board.PrefName = actor.PreferredUsername + data.Board.Summary = actor.Summary + data.Board.InReplyTo = "" + data.Board.To = actor.Outbox + data.Board.Actor = actor + data.Board.ModCred, _ = util.GetPasswordFromSession(ctx) + data.Board.Domain = config.Domain + data.Board.Restricted = actor.Restricted + data.CurrentPage = page + data.ReturnTo = "feed" + + data.Board.Post.Actor = actor.Id + + capt, err := util.GetRandomCaptcha() + if err != nil { + return util.MakeError(err, "OutboxGet") + } + data.Board.Captcha = config.Domain + "/" + capt + data.Board.CaptchaCode = post.GetCaptchaCode(data.Board.Captcha) + + data.Title = "/" + actor.Name + "/ - " + actor.PreferredUsername + + data.Key = config.Key + + data.Boards = webfinger.Boards + data.Posts = collection.OrderedItems + + data.Pages = pages + data.TotalPage = len(data.Pages) - 1 + + data.Meta.Description = data.Board.Summary + data.Meta.Url = data.Board.Actor.Id + data.Meta.Title = data.Title + + data.Themes = &config.Themes + data.ThemeCookie = route.GetThemeCookie(ctx) + + return ctx.Render("nposts", fiber.Map{ + "page": data, + }, "layouts/main") +} + +func ActorArchiveGet(ctx *fiber.Ctx) error { + collection := ctx.Locals("collection").(activitypub.Collection) + actor := collection.Actor + + var returnData route.PageData + returnData.Board.Name = actor.Name + returnData.Board.PrefName = actor.PreferredUsername + returnData.Board.InReplyTo = "" + returnData.Board.To = actor.Outbox + returnData.Board.Actor = actor + returnData.Board.Summary = actor.Summary + returnData.Board.ModCred, _ = util.GetPasswordFromSession(ctx) + returnData.Board.Domain = config.Domain + returnData.Board.Restricted = actor.Restricted + returnData.Key = config.Key + returnData.ReturnTo = "archive" + + returnData.Board.Post.Actor = actor.Id + + var err error + returnData.Instance, err = activitypub.GetActorFromDB(config.Domain) + + capt, err := util.GetRandomCaptcha() + if err != nil { + return util.MakeError(err, "ArchiveGet") + } + returnData.Board.Captcha = config.Domain + "/" + capt + returnData.Board.CaptchaCode = post.GetCaptchaCode(returnData.Board.Captcha) + + returnData.Title = "/" + actor.Name + "/ - " + actor.PreferredUsername + + returnData.Boards = webfinger.Boards + + returnData.Posts = collection.OrderedItems + + returnData.Themes = &config.Themes + returnData.ThemeCookie = route.GetThemeCookie(ctx) + + return ctx.Render("archive", fiber.Map{ + "page": returnData, + }, "layouts/main") +} diff --git a/route/routes/admin.go b/route/routes/admin.go new file mode 100644 index 0000000..86e12c6 --- /dev/null +++ b/route/routes/admin.go @@ -0,0 +1,291 @@ +package routes + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "time" + + "github.com/FChannel0/FChannel-Server/activitypub" + "github.com/FChannel0/FChannel-Server/config" + "github.com/FChannel0/FChannel-Server/db" + "github.com/FChannel0/FChannel-Server/route" + "github.com/FChannel0/FChannel-Server/util" + "github.com/FChannel0/FChannel-Server/webfinger" + "github.com/gofiber/fiber/v2" +) + +func AdminVerify(ctx *fiber.Ctx) error { + identifier := ctx.FormValue("id") + code := ctx.FormValue("code") + + var verify util.Verify + verify.Identifier = identifier + verify.Code = code + + j, _ := json.Marshal(&verify) + + req, err := http.NewRequest("POST", config.Domain+"/auth", bytes.NewBuffer(j)) + + if err != nil { + return util.MakeError(err, "AdminVerify") + } + + req.Header.Set("Content-Type", config.ActivityStreams) + + resp, err := http.DefaultClient.Do(req) + + if err != nil { + return util.MakeError(err, "AdminVerify") + } + + defer resp.Body.Close() + + rBody, _ := ioutil.ReadAll(resp.Body) + + body := string(rBody) + + if resp.StatusCode != 200 { + return ctx.Redirect("/"+config.Key, http.StatusPermanentRedirect) + } + + ctx.Cookie(&fiber.Cookie{ + Name: "session_token", + Value: body + "|" + verify.Code, + Expires: time.Now().UTC().Add(60 * 60 * 48 * time.Second), + }) + + return ctx.Redirect("/", http.StatusSeeOther) +} + +// TODO remove this route it is mostly unneeded +func AdminAuth(ctx *fiber.Ctx) error { + var verify util.Verify + + err := json.Unmarshal(ctx.Body(), &verify) + + if err != nil { + return util.MakeError(err, "AdminAuth") + } + + v, _ := util.GetVerificationByCode(verify.Code) + + if v.Identifier == verify.Identifier { + _, err := ctx.Write([]byte(v.Board)) + return util.MakeError(err, "AdminAuth") + } + + ctx.Response().Header.SetStatusCode(http.StatusBadRequest) + _, err = ctx.Write([]byte("")) + + return util.MakeError(err, "AdminAuth") +} + +func AdminIndex(ctx *fiber.Ctx) error { + id, _ := util.GetPasswordFromSession(ctx) + actor, _ := webfinger.GetActorFromPath(ctx.Path(), "/"+config.Key+"/") + + if actor.Id == "" { + actor, _ = activitypub.GetActorByNameFromDB(config.Domain) + } + + if id == "" || (id != actor.Id && id != config.Domain) { + return ctx.Render("verify", fiber.Map{}) + } + + actor, err := activitypub.GetActor(config.Domain) + + if err != nil { + return util.MakeError(err, "AdminIndex") + } + + reqActivity := activitypub.Activity{Id: actor.Following} + follow, _ := reqActivity.GetCollection() + follower, _ := reqActivity.GetCollection() + + var following []string + var followers []string + + for _, e := range follow.Items { + following = append(following, e.Id) + } + + for _, e := range follower.Items { + followers = append(followers, e.Id) + } + + var adminData route.AdminPage + adminData.Following = following + adminData.Followers = followers + adminData.Actor = actor.Id + adminData.Key = config.Key + adminData.Domain = config.Domain + adminData.Board.ModCred, _ = util.GetPasswordFromSession(ctx) + adminData.Title = actor.Name + " Admin page" + + adminData.Boards = webfinger.Boards + + adminData.Board.Post.Actor = actor.Id + + adminData.PostBlacklist, _ = util.GetRegexBlacklist() + + adminData.Themes = &config.Themes + + return ctx.Render("admin", fiber.Map{ + "page": adminData, + }) +} + +func AdminFollow(ctx *fiber.Ctx) error { + follow := ctx.FormValue("follow") + actorId := ctx.FormValue("actor") + + actor := activitypub.Actor{Id: actorId} + followActivity, _ := actor.MakeFollowActivity(follow) + + objActor := activitypub.Actor{Id: followActivity.Object.Actor} + + if isLocal, _ := objActor.IsLocal(); !isLocal && followActivity.Actor.Id == config.Domain { + _, err := ctx.Write([]byte("main board can only follow local boards. Create a new board and then follow outside boards from it.")) + return util.MakeError(err, "AdminIndex") + } + + if actor, _ := activitypub.FingerActor(follow); actor.Id != "" { + if err := followActivity.MakeRequestOutbox(); err != nil { + return util.MakeError(err, "AdminFollow") + } + } + + var redirect string + actor, _ = webfinger.GetActorFromPath(ctx.Path(), "/"+config.Key+"/") + + if actor.Name != "main" { + redirect = actor.Name + } + + return ctx.Redirect("/"+config.Key+"/"+redirect, http.StatusSeeOther) +} + +func AdminAddBoard(ctx *fiber.Ctx) error { + actor, _ := activitypub.GetActorFromDB(config.Domain) + + if hasValidation := actor.HasValidation(ctx); !hasValidation { + return nil + } + + var newActorActivity activitypub.Activity + var board activitypub.Actor + + var restrict bool + if ctx.FormValue("restricted") == "True" { + restrict = true + } else { + restrict = false + } + + board.Name = ctx.FormValue("name") + board.PreferredUsername = ctx.FormValue("prefname") + board.Summary = ctx.FormValue("summary") + board.Restricted = restrict + + newActorActivity.AtContext.Context = "https://www.w3.org/ns/activitystreams" + newActorActivity.Type = "New" + + var nobj activitypub.ObjectBase + newActorActivity.Actor = &actor + newActorActivity.Object = &nobj + + newActorActivity.Object.Alias = board.Name + newActorActivity.Object.Name = board.PreferredUsername + newActorActivity.Object.Summary = board.Summary + newActorActivity.Object.Sensitive = board.Restricted + + newActorActivity.MakeRequestOutbox() + return ctx.Redirect("/"+config.Key, http.StatusSeeOther) +} + +func AdminPostNews(c *fiber.Ctx) error { + // STUB + + return c.SendString("admin post news") +} + +func AdminNewsDelete(c *fiber.Ctx) error { + // STUB + + return c.SendString("admin news delete") +} + +func AdminActorIndex(ctx *fiber.Ctx) error { + actor, _ := webfinger.GetActorFromPath(ctx.Path(), "/"+config.Key+"/") + + reqActivity := activitypub.Activity{Id: actor.Following} + follow, _ := reqActivity.GetCollection() + + reqActivity.Id = actor.Followers + follower, _ := reqActivity.GetCollection() + + reqActivity.Id = actor.Id + "/reported" + reported, _ := activitypub.GetActorCollectionReq(reqActivity.Id) + + var following []string + var followers []string + var reports []db.Report + + for _, e := range follow.Items { + following = append(following, e.Id) + } + + for _, e := range follower.Items { + followers = append(followers, e.Id) + } + + for _, e := range reported.Items { + var r db.Report + r.Count = int(e.Size) + r.ID = e.Id + r.Reason = e.Content + reports = append(reports, r) + } + + localReports, _ := db.GetLocalReport(actor.Name) + + for _, e := range localReports { + var r db.Report + r.Count = e.Count + r.ID = e.ID + r.Reason = e.Reason + reports = append(reports, r) + } + + var data route.AdminPage + data.Following = following + data.Followers = followers + data.Reported = reports + data.Domain = config.Domain + data.IsLocal, _ = actor.IsLocal() + + data.Title = "Manage /" + actor.Name + "/" + data.Boards = webfinger.Boards + data.Board.Name = actor.Name + data.Board.Actor = actor + data.Key = config.Key + data.Board.TP = config.TP + + data.Board.Post.Actor = actor.Id + + data.AutoSubscribe, _ = actor.GetAutoSubscribe() + + data.Themes = &config.Themes + + data.RecentPosts, _ = actor.GetRecentPosts() + + if cookie := ctx.Cookies("theme"); cookie != "" { + data.ThemeCookie = cookie + } + + return ctx.Render("manage", fiber.Map{ + "page": data, + }) +} diff --git a/route/routes/api.go b/route/routes/api.go new file mode 100644 index 0000000..080d88d --- /dev/null +++ b/route/routes/api.go @@ -0,0 +1,55 @@ +package routes + +import ( + "io/ioutil" + "net/http" + "time" + + "github.com/FChannel0/FChannel-Server/config" + "github.com/FChannel0/FChannel-Server/util" + "github.com/gofiber/fiber/v2" +) + +func Media(c *fiber.Ctx) error { + if c.Query("hash") != "" { + return RouteImages(c, c.Query("hash")) + } + + return c.SendStatus(404) +} + +func RouteImages(ctx *fiber.Ctx, media string) error { + req, err := http.NewRequest("GET", config.MediaHashs[media], nil) + if err != nil { + return util.MakeError(err, "RouteImages") + } + + client := http.Client{ + Timeout: 5 * time.Second, + } + + resp, err := client.Do(req) + if err != nil { + return util.MakeError(err, "RouteImages") + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + fileBytes, err := ioutil.ReadFile("./static/notfound.png") + if err != nil { + return util.MakeError(err, "RouteImages") + } + + _, err = ctx.Write(fileBytes) + return util.MakeError(err, "RouteImages") + } + + body, _ := ioutil.ReadAll(resp.Body) + for name, values := range resp.Header { + for _, value := range values { + ctx.Append(name, value) + } + } + + return ctx.Send(body) +} diff --git a/route/routes/boardmgmt.go b/route/routes/boardmgmt.go new file mode 100644 index 0000000..15b2686 --- /dev/null +++ b/route/routes/boardmgmt.go @@ -0,0 +1,47 @@ +package routes + +import "github.com/gofiber/fiber/v2" + +func BoardBanMedia(ctx *fiber.Ctx) error { + return ctx.SendString("board ban media") +} + +func BoardDelete(ctx *fiber.Ctx) error { + return ctx.SendString("board delete") +} + +func BoardDeleteAttach(ctx *fiber.Ctx) error { + return ctx.SendString("board delete attach") +} + +func BoardMarkSensitive(ctx *fiber.Ctx) error { + return ctx.SendString("board mark sensitive") +} + +func BoardRemove(ctx *fiber.Ctx) error { + return ctx.SendString("board remove") +} + +func BoardRemoveAttach(ctx *fiber.Ctx) error { + return ctx.SendString("board remove attach") +} + +func BoardAddToIndex(ctx *fiber.Ctx) error { + return ctx.SendString("board add to index") +} + +func BoardPopArchive(ctx *fiber.Ctx) error { + return ctx.SendString("board pop archive") +} + +func BoardAutoSubscribe(ctx *fiber.Ctx) error { + return ctx.SendString("board auto subscribe") +} + +func BoardBlacklist(ctx *fiber.Ctx) error { + return ctx.SendString("board blacklist") +} + +func BoardReport(ctx *fiber.Ctx) error { + return ctx.SendString("board report") +} diff --git a/route/routes/main.go b/route/routes/main.go new file mode 100644 index 0000000..99dad31 --- /dev/null +++ b/route/routes/main.go @@ -0,0 +1,100 @@ +package routes + +import ( + "github.com/FChannel0/FChannel-Server/activitypub" + "github.com/FChannel0/FChannel-Server/config" + "github.com/FChannel0/FChannel-Server/db" + "github.com/FChannel0/FChannel-Server/route" + "github.com/FChannel0/FChannel-Server/util" + "github.com/FChannel0/FChannel-Server/webfinger" + "github.com/gofiber/fiber/v2" +) + +func Index(ctx *fiber.Ctx) error { + actor, err := activitypub.GetActorFromDB(config.Domain) + if err != nil { + return util.MakeError(err, "Index") + } + + // this is a activitpub json request return json instead of html page + if activitypub.AcceptActivity(ctx.Get("Accept")) { + actor.GetInfoResp(ctx) + return nil + } + + var data route.PageData + + reqActivity := activitypub.Activity{Id: "https://fchan.xyz/followers"} + col, err := reqActivity.GetCollection() + if err != nil { + return util.MakeError(err, "Index") + } + + if len(col.Items) > 0 { + data.InstanceIndex = col.Items + } + + data.NewsItems, err = db.GetNews(3) + if err != nil { + return util.MakeError(err, "Index") + } + + data.Title = "Welcome to " + actor.PreferredUsername + data.PreferredUsername = actor.PreferredUsername + data.Boards = webfinger.Boards + data.Board.Name = "" + data.Key = config.Key + data.Board.Domain = config.Domain + data.Board.ModCred, _ = util.GetPasswordFromSession(ctx) + data.Board.Actor = actor + data.Board.Post.Actor = actor.Id + data.Board.Restricted = actor.Restricted + //almost certainly there is a better algorithm for this but the old one was wrong + //and I suck at math. This works at least. + data.BoardRemainer = make([]int, 3-(len(data.Boards)%3)) + + if len(data.BoardRemainer) == 3 { + data.BoardRemainer = make([]int, 0) + } + + data.Meta.Description = data.PreferredUsername + " a federated image board based on ActivityPub. The current version of the code running on the server is still a work-in-progress product, expect a bumpy ride for the time being. Get the server code here: https://github.com/FChannel0." + data.Meta.Url = data.Board.Domain + data.Meta.Title = data.Title + + data.Themes = &config.Themes + data.ThemeCookie = route.GetThemeCookie(ctx) + + return ctx.Render("index", fiber.Map{ + "page": data, + }, "layouts/main") +} + +func Inbox(ctx *fiber.Ctx) error { + // TODO main actor Inbox route + return ctx.SendString("main inbox") +} + +func Outbox(ctx *fiber.Ctx) error { + actor, err := webfinger.GetActorFromPath(ctx.Path(), "/") + + if err != nil { + return util.MakeError(err, "Outbox") + } + + if activitypub.AcceptActivity(ctx.Get("Accept")) { + actor.GetOutbox(ctx) + return nil + } + + return route.ParseOutboxRequest(ctx, actor) +} + +func Following(ctx *fiber.Ctx) error { + actor, _ := activitypub.GetActorFromDB(config.Domain) + return actor.GetFollowingResp(ctx) +} + +func Followers(ctx *fiber.Ctx) error { + actor, _ := activitypub.GetActorFromDB(config.Domain) + return actor.GetFollowersResp(ctx) +} diff --git a/route/routes/news.go b/route/routes/news.go new file mode 100644 index 0000000..0d226a5 --- /dev/null +++ b/route/routes/news.go @@ -0,0 +1,73 @@ +package routes + +import ( + "github.com/FChannel0/FChannel-Server/activitypub" + "github.com/FChannel0/FChannel-Server/config" + "github.com/FChannel0/FChannel-Server/db" + "github.com/FChannel0/FChannel-Server/route" + "github.com/FChannel0/FChannel-Server/util" + "github.com/FChannel0/FChannel-Server/webfinger" + "github.com/gofiber/fiber/v2" +) + +func NewsGet(ctx *fiber.Ctx) error { + timestamp := 0 + + actor, err := activitypub.GetActorFromDB(config.Domain) + if err != nil { + return util.MakeError(err, "NewsGet") + } + + var data route.PageData + data.PreferredUsername = actor.PreferredUsername + data.Boards = webfinger.Boards + data.Board.Name = "" + data.Key = config.Key + data.Board.Domain = config.Domain + data.Board.ModCred, _ = util.GetPasswordFromSession(ctx) + data.Board.Actor = actor + data.Board.Post.Actor = actor.Id + data.Board.Restricted = actor.Restricted + data.NewsItems = make([]db.NewsItem, 1) + + data.NewsItems[0], err = db.GetNewsItem(timestamp) + if err != nil { + return util.MakeError(err, "NewsGet") + } + + data.Title = actor.PreferredUsername + ": " + data.NewsItems[0].Title + + data.Themes = &config.Themes + data.ThemeCookie = route.GetThemeCookie(ctx) + + return ctx.Render("news", fiber.Map{"page": data}, "layouts/main") +} + +func AllNewsGet(ctx *fiber.Ctx) error { + actor, err := activitypub.GetActorFromDB(config.Domain) + if err != nil { + return util.MakeError(err, "AllNewsGet") + } + + var data route.PageData + data.PreferredUsername = actor.PreferredUsername + data.Title = actor.PreferredUsername + " News" + data.Boards = webfinger.Boards + data.Board.Name = "" + data.Key = config.Key + data.Board.Domain = config.Domain + data.Board.ModCred, _ = util.GetPasswordFromSession(ctx) + data.Board.Actor = actor + data.Board.Post.Actor = actor.Id + data.Board.Restricted = actor.Restricted + + data.NewsItems, err = db.GetNews(0) + if err != nil { + return util.MakeError(err, "AllNewsGet") + } + + data.Themes = &config.Themes + data.ThemeCookie = route.GetThemeCookie(ctx) + + return ctx.Render("anews", fiber.Map{"page": data}, "layouts/main") +} diff --git a/route/routes/webfinger.go b/route/routes/webfinger.go new file mode 100644 index 0000000..3d5fa63 --- /dev/null +++ b/route/routes/webfinger.go @@ -0,0 +1,58 @@ +package routes + +import ( + "encoding/json" + "strings" + + "github.com/FChannel0/FChannel-Server/activitypub" + "github.com/FChannel0/FChannel-Server/config" + "github.com/FChannel0/FChannel-Server/util" + "github.com/gofiber/fiber/v2" +) + +func Webfinger(c *fiber.Ctx) error { + acct := c.Query("resource") + + if len(acct) < 1 { + c.Status(fiber.StatusBadRequest) + return c.Send([]byte("resource needs a value")) + } + + acct = strings.Replace(acct, "acct:", "", -1) + + actorDomain := strings.Split(acct, "@") + + if len(actorDomain) < 2 { + c.Status(fiber.StatusBadRequest) + return c.Send([]byte("accepts only subject form of acct:board@instance")) + } + + if actorDomain[0] == "main" { + actorDomain[0] = "" + } else { + actorDomain[0] = "/" + actorDomain[0] + } + + actor := activitypub.Actor{Id: config.TP + "" + actorDomain[1] + "" + actorDomain[0]} + if res, err := actor.IsLocal(); err == nil && !res { + c.Status(fiber.StatusBadRequest) + return c.Send([]byte("actor not local")) + } else if err != nil { + return util.MakeError(err, "Webfinger") + } + + var finger activitypub.Webfinger + var link activitypub.WebfingerLink + + finger.Subject = "acct:" + actorDomain[0] + "@" + actorDomain[1] + link.Rel = "self" + link.Type = "application/activity+json" + link.Href = config.TP + "" + actorDomain[1] + "" + actorDomain[0] + + finger.Links = append(finger.Links, link) + + enc, _ := json.Marshal(finger) + + c.Set("Content-Type", config.ActivityStreams) + return c.Send(enc) +} diff --git a/route/structs.go b/route/structs.go new file mode 100644 index 0000000..7b1c2b8 --- /dev/null +++ b/route/structs.go @@ -0,0 +1,56 @@ +package route + +import ( + "github.com/FChannel0/FChannel-Server/activitypub" + "github.com/FChannel0/FChannel-Server/db" + "github.com/FChannel0/FChannel-Server/util" + "github.com/FChannel0/FChannel-Server/webfinger" +) + +type PageData struct { + Title string + PreferredUsername string + Board webfinger.Board + Pages []int + CurrentPage int + TotalPage int + Boards []webfinger.Board + Posts []activitypub.ObjectBase + Key string + PostId string + Instance activitypub.Actor + InstanceIndex []activitypub.ObjectBase + ReturnTo string + NewsItems []db.NewsItem + BoardRemainer []int + Meta Meta + + Themes *[]string + ThemeCookie string +} + +type AdminPage struct { + Title string + Board webfinger.Board + Key string + Actor string + Boards []webfinger.Board + Following []string + Followers []string + Reported []db.Report + Domain string + IsLocal bool + PostBlacklist []util.PostBlacklist + AutoSubscribe bool + RecentPosts []activitypub.ObjectBase + + Themes *[]string + ThemeCookie string +} + +type Meta struct { + Title string + Description string + Url string + Preview string +} diff --git a/route/util.go b/route/util.go new file mode 100644 index 0000000..5bd15d6 --- /dev/null +++ b/route/util.go @@ -0,0 +1,377 @@ +package route + +import ( + "encoding/json" + "fmt" + "html/template" + "regexp" + "strings" + "time" + + "github.com/FChannel0/FChannel-Server/activitypub" + "github.com/FChannel0/FChannel-Server/config" + "github.com/FChannel0/FChannel-Server/db" + "github.com/FChannel0/FChannel-Server/post" + "github.com/FChannel0/FChannel-Server/util" + "github.com/FChannel0/FChannel-Server/webfinger" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/html" +) + +func GetThemeCookie(c *fiber.Ctx) string { + cookie := c.Cookies("theme") + if cookie != "" { + cookies := strings.SplitN(cookie, "=", 2) + return cookies[0] + } + + return "default" +} + +func WantToServeCatalog(actorName string) (activitypub.Collection, bool, error) { + var collection activitypub.Collection + serve := false + + actor, err := activitypub.GetActorByNameFromDB(actorName) + if err != nil { + return collection, false, util.MakeError(err, "WantToServeCatalog") + } + + if actor.Id != "" { + collection, err = actor.GetCatalogCollection() + if err != nil { + return collection, false, util.MakeError(err, "WantToServeCatalog") + } + + collection.Actor = actor + return collection, true, nil + } + + return collection, serve, nil +} + +func WantToServeArchive(actorName string) (activitypub.Collection, bool, error) { + var collection activitypub.Collection + serve := false + + actor, err := activitypub.GetActorByNameFromDB(actorName) + if err != nil { + return collection, false, util.MakeError(err, "WantToServeArchive") + } + + if actor.Id != "" { + collection, err = actor.GetCollectionType("Archive") + if err != nil { + return collection, false, util.MakeError(err, "WantToServeArchive") + } + + collection.Actor = actor + return collection, true, nil + } + + return collection, serve, nil +} + +func GetActorPost(ctx *fiber.Ctx, path string) error { + obj := activitypub.ObjectBase{Id: config.Domain + "" + path} + collection, err := obj.GetCollectionFromPath() + + if err != nil { + return util.MakeError(err, "GetActorPost") + } + + if len(collection.OrderedItems) > 0 { + enc, err := json.MarshalIndent(collection, "", "\t") + if err != nil { + return util.MakeError(err, "GetActorPost") + } + + ctx.Response().Header.Set("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") + _, err = ctx.Write(enc) + return util.MakeError(err, "GetActorPost") + } + + return nil +} + +func ParseOutboxRequest(ctx *fiber.Ctx, actor activitypub.Actor) error { + contentType := util.GetContentType(ctx.Get("content-type")) + + if contentType == "multipart/form-data" || contentType == "application/x-www-form-urlencoded" { + hasCaptcha, err := util.BoardHasAuthType(actor.Name, "captcha") + if err != nil { + return util.MakeError(err, "ParseOutboxRequest") + } + + valid, err := post.CheckCaptcha(ctx.FormValue("captcha")) + if err == nil && hasCaptcha && valid { + header, _ := ctx.FormFile("file") + if header != nil { + f, _ := header.Open() + defer f.Close() + if header.Size > (7 << 20) { + ctx.Response().Header.SetStatusCode(403) + _, err := ctx.Write([]byte("7MB max file size")) + return util.MakeError(err, "ParseOutboxRequest") + } else if isBanned, err := post.IsMediaBanned(f); err == nil && isBanned { + //Todo add logging + config.Log.Println("media banned") + ctx.Response().Header.SetStatusCode(403) + _, err := ctx.Write([]byte("media banned")) + return util.MakeError(err, "ParseOutboxRequest") + } else if err != nil { + return util.MakeError(err, "ParseOutboxRequest") + } + + contentType, _ := util.GetFileContentType(f) + + if !post.SupportedMIMEType(contentType) { + ctx.Response().Header.SetStatusCode(403) + _, err := ctx.Write([]byte("file type not supported")) + return util.MakeError(err, "ParseOutboxRequest") + } + } + + var nObj = activitypub.CreateObject("Note") + nObj, err := post.ObjectFromForm(ctx, nObj) + if err != nil { + return util.MakeError(err, "ParseOutboxRequest") + } + + nObj.Actor = config.Domain + "/" + actor.Name + + nObj, err = nObj.Write() + if err != nil { + return util.MakeError(err, "ParseOutboxRequest") + } + + if len(nObj.To) == 0 { + if err := actor.ArchivePosts(); err != nil { + return util.MakeError(err, "ParseOutboxRequest") + } + } + + activity, err := nObj.CreateActivity("Create") + if err != nil { + return util.MakeError(err, "ParseOutboxRequest") + } + + activity, err = activity.AddFollowersTo() + if err != nil { + return util.MakeError(err, "ParseOutboxRequest") + } + + go activity.MakeRequestInbox() + + var id string + op := len(nObj.InReplyTo) - 1 + if op >= 0 { + if nObj.InReplyTo[op].Id == "" { + id = nObj.Id + } else { + id = nObj.InReplyTo[0].Id + "|" + nObj.Id + } + } + + ctx.Response().Header.Set("Status", "200") + _, err = ctx.Write([]byte(id)) + return util.MakeError(err, "ParseOutboxRequest") + } + + ctx.Response().Header.Set("Status", "403") + _, err = ctx.Write([]byte("captcha could not auth")) + return util.MakeError(err, "") + } else { // json request + activity, err := activitypub.GetActivityFromJson(ctx) + if err != nil { + return util.MakeError(err, "ParseOutboxRequest") + } + + if res, err := activity.IsLocal(); err == nil && res { + if res := activity.Actor.VerifyHeaderSignature(ctx); err == nil && !res { + ctx.Response().Header.Set("Status", "403") + _, err = ctx.Write([]byte("")) + return util.MakeError(err, "ParseOutboxRequest") + } + + switch activity.Type { + case "Create": + ctx.Response().Header.Set("Status", "403") + _, err = ctx.Write([]byte("")) + break + + case "Follow": + validActor := (activity.Object.Actor != "") + validLocalActor := (activity.Actor.Id == actor.Id) + + var rActivity activitypub.Activity + + if validActor && validLocalActor { + rActivity = activity.AcceptFollow() + rActivity, err = rActivity.SetActorFollowing() + + if err != nil { + return util.MakeError(err, "ParseOutboxRequest") + } + + if err := activity.MakeRequestInbox(); err != nil { + return util.MakeError(err, "ParseOutboxRequest") + } + } + + actor, _ := activitypub.GetActorFromDB(config.Domain) + webfinger.FollowingBoards, err = actor.GetFollowing() + + if err != nil { + return util.MakeError(err, "ParseOutboxRequest") + } + + webfinger.Boards, err = webfinger.GetBoardCollection() + + if err != nil { + return util.MakeError(err, "ParseOutboxRequest") + } + break + + case "Delete": + config.Log.Println("This is a delete") + ctx.Response().Header.Set("Status", "403") + _, err = ctx.Write([]byte("could not process activity")) + break + + case "Note": + ctx.Response().Header.Set("Satus", "403") + _, err = ctx.Write([]byte("could not process activity")) + break + + case "New": + name := activity.Object.Alias + prefname := activity.Object.Name + summary := activity.Object.Summary + restricted := activity.Object.Sensitive + + actor, err := db.CreateNewBoard(*activitypub.CreateNewActor(name, prefname, summary, config.AuthReq, restricted)) + if err != nil { + return util.MakeError(err, "ParseOutboxRequest") + } + + if actor.Id != "" { + var board []activitypub.ObjectBase + var item activitypub.ObjectBase + var removed bool = false + + item.Id = actor.Id + for _, e := range webfinger.FollowingBoards { + if e.Id != item.Id { + board = append(board, e) + } else { + removed = true + } + } + + if !removed { + board = append(board, item) + } + + webfinger.FollowingBoards = board + webfinger.Boards, err = webfinger.GetBoardCollection() + return util.MakeError(err, "ParseOutboxRequest") + } + + ctx.Response().Header.Set("Status", "403") + _, err = ctx.Write([]byte("")) + break + + default: + ctx.Response().Header.Set("status", "403") + _, err = ctx.Write([]byte("could not process activity")) + } + } else if err != nil { + return util.MakeError(err, "ParseOutboxRequest") + } else { + config.Log.Println("is NOT activity") + ctx.Response().Header.Set("Status", "403") + _, err = ctx.Write([]byte("could not process activity")) + return util.MakeError(err, "ParseOutboxRequest") + } + } + + return nil +} + +func TemplateFunctions(engine *html.Engine) { + engine.AddFunc("mod", func(i, j int) bool { + return i%j == 0 + }) + + engine.AddFunc("sub", func(i, j int) int { + return i - j + }) + + engine.AddFunc("add", func(i, j int) int { + return i + j + }) + + engine.AddFunc("unixtoreadable", func(u int) string { + return time.Unix(int64(u), 0).Format("Jan 02, 2006") + }) + + engine.AddFunc("timeToReadableLong", func(t time.Time) string { + return t.Format("01/02/06(Mon)15:04:05") + }) + + engine.AddFunc("timeToUnix", func(t time.Time) string { + return fmt.Sprint(t.Unix()) + }) + + engine.AddFunc("proxy", util.MediaProxy) + + // previously short + engine.AddFunc("shortURL", util.ShortURL) + + engine.AddFunc("parseAttachment", post.ParseAttachment) + + engine.AddFunc("parseContent", post.ParseContent) + + engine.AddFunc("shortImg", util.ShortImg) + + engine.AddFunc("convertSize", util.ConvertSize) + + engine.AddFunc("isOnion", util.IsOnion) + + engine.AddFunc("parseReplyLink", func(actorId string, op string, id string, content string) template.HTML { + actor, _ := activitypub.FingerActor(actorId) + title := strings.ReplaceAll(post.ParseLinkTitle(actor.Id+"/", op, content), `/\<`, ">") + link := "<a href=\"/" + actor.Name + "/" + util.ShortURL(actor.Outbox, op) + "#" + util.ShortURL(actor.Outbox, id) + "\" title=\"" + title + "\" class=\"replyLink\">>>" + util.ShortURL(actor.Outbox, id) + "</a>" + return template.HTML(link) + }) + + engine.AddFunc("shortExcerpt", func(post activitypub.ObjectBase) string { + var returnString string + + if post.Name != "" { + returnString = post.Name + "| " + post.Content + } else { + returnString = post.Content + } + + re := regexp.MustCompile(`(^(.|\r\n|\n){100})`) + + match := re.FindStringSubmatch(returnString) + + if len(match) > 0 { + returnString = match[0] + "..." + } + + re = regexp.MustCompile(`(^.+\|)`) + + match = re.FindStringSubmatch(returnString) + + if len(match) > 0 { + returnString = strings.Replace(returnString, match[0], "<b>"+match[0]+"</b>", 1) + returnString = strings.Replace(returnString, "|", ":", 1) + } + + return returnString + }) +} |