package main import ( "encoding/json" "fmt" "github.com/gorilla/mux" "log" "net/http" "net/url" "os" "regexp" "strings" "time" "willnorris.com/go/webmention" ) func Serve() { hookHandler := func(w http.ResponseWriter, r *http.Request) { fmt.Println("Fetch feeds: ", time.Now().Format(time.RFC3339)) go func() { for _, actor := range actors { fmt.Println(actor.feed) articles, err := allFeedItems(actor.feed) if err != nil { fmt.Println(actor.feed, err.Error()) continue } if len(articles) < 1 { fmt.Println(actor.feed, "Empty feed") continue } err = actor.PostArticle(articles[0]) if err != nil { fmt.Println("Posting", articles[0], "failed") } } }() } webfingerHandler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set("content-type", "application/jrd+json; charset=utf-8") name := r.URL.Query().Get("resource") // should be something like acct:user@example.com urlBaseUrl, _ := url.Parse(baseURL) re := regexp.MustCompile(`^acct:(.*)@` + regexp.QuoteMeta(urlBaseUrl.Host) + `$`) name = re.ReplaceAllString(name, "$1") actor := actors[name] if actor == nil && len(defaultActor) > 0 { // return default actor actor = actors[defaultActor] } else if actor == nil { // error out if this actor does not exist w.WriteHeader(http.StatusNotFound) return } responseMap := make(map[string]interface{}) responseMap["subject"] = "acct:" + actor.Name + "@" + urlBaseUrl.Host // links is a json array with a single element var links [1]map[string]string link1 := make(map[string]string) link1["rel"] = "self" link1["type"] = ContentTypeAs2 link1["href"] = actor.iri links[0] = link1 responseMap["links"] = links _ = json.NewEncoder(w).Encode(responseMap) } inboxHandler := func(w http.ResponseWriter, r *http.Request) { activity := make(map[string]interface{}) err := json.NewDecoder(r.Body).Decode(&activity) _ = r.Body.Close() if err != nil { w.WriteHeader(http.StatusInternalServerError) return } actor := actors[mux.Vars(r)["actor"]] if actor == nil { // actor doesn't exist w.WriteHeader(http.StatusNotFound) return } switch activity["type"] { case "Follow": actor.Accept(activity) case "Undo": { if object, ok := activity["object"].(map[string]interface{}); ok { if objectType, ok := object["type"].(string); ok && objectType == "Follow" { if iri, ok := object["actor"].(string); ok && iri == activity["actor"] { _ = actor.RemoveFollower(iri) fmt.Println(iri, "unfollowed") if telegramBot != nil { _ = telegramBot.Post(iri + " unfollowed") } } } } } case "Create": { if object, ok := activity["object"].(map[string]interface{}); ok { inReplyTo, hasReplyToString := object["inReplyTo"].(string) id, hasId := object["id"].(string) if hasReplyToString && hasId && len(inReplyTo) > 0 && len(id) > 0 && strings.Contains(inReplyTo, actor.iri) { // It's an ActivityPub reply fmt.Println("Received reply to:", inReplyTo) webmentionClient := webmention.New(nil) sendWebmention(webmentionClient, actor, id, inReplyTo) } else if hasId && len(id) > 0 { // May be a mention webmentionClient := webmention.New(nil) dl, err := webmentionClient.DiscoverLinks(id, "") if err != nil { return } // Send Webmentions sent := map[string]bool{} for _, link := range dl { if !sent[link] { sendWebmention(webmentionClient, actor, id, link) sent[link] = true } } } } } case "Delete": { if object, ok := activity["object"].(string); ok && len(object) > 0 && activity["actor"] == object { _ = actor.RemoveFollower(object) } } case "Like": { likeActor, likeActorOk := activity["actor"].(string) likeObject, likeObjectOk := activity["object"].(string) if likeActorOk && likeObjectOk && len(likeActor) > 0 && len(likeObject) > 0 && strings.Contains(likeObject, actor.iri) { fmt.Println(likeActor, "liked", likeObject) if telegramBot != nil { _ = telegramBot.Post(likeActor + " liked " + likeObject) } } } case "Announce": { announceActor, announceActorOk := activity["actor"].(string) announceObject, announceObjectOk := activity["object"].(string) if announceActorOk && announceObjectOk && len(announceActor) > 0 && len(announceObject) > 0 && strings.Contains(announceObject, actor.iri) { fmt.Println(announceActor, "announced", announceObject) if telegramBot != nil { _ = telegramBot.Post(announceActor + " announced " + announceObject) } } } default: // Log inbox request logInbox(actor, activity) } // Return 201 w.WriteHeader(http.StatusCreated) } // Add the handlers to a HTTP server gorilla := mux.NewRouter() gorilla.HandleFunc("/hook", hookHandler).Methods(http.MethodPost) gorilla.HandleFunc("/.well-known/webfinger", webfingerHandler) gorilla.PathPrefix("/{actor}/inbox").HandlerFunc(inboxHandler).Methods(http.MethodPost) http.Handle("/", gorilla) log.Fatal(http.ListenAndServe(":8081", nil)) } func logInbox(actor *Actor, activity map[string]interface{}) { f, err := os.OpenFile(storage+slash+"actors"+slash+actor.Name+slash+"inbox", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) defer func() { _ = f.Close() }() if err != nil { return } _ = json.NewEncoder(f).Encode(activity) _, _ = f.WriteString("\n---\n") } func sendWebmention(client *webmention.Client, actor *Actor, mentioningLink, mentionedLink string) { if !strings.Contains(mentionedLink, actor.iri) { // Not mention of blog return } endpoint, err := client.DiscoverEndpoint(mentionedLink) if err != nil || len(endpoint) < 1 { return } _, err = client.SendWebmention(endpoint, mentioningLink, mentionedLink) if err != nil { log.Println("Sending webmention to " + mentionedLink + " failed") return } log.Println("Sent webmention to " + mentionedLink) }