1package activityserve 2 3import ( 4 "fmt" 5 "io/ioutil" 6 "net/http" 7 "strconv" 8 "strings" 9 10 "github.com/gologme/log" 11 "github.com/gorilla/mux" 12 "github.com/writefreely/go-nodeinfo" 13 14 "encoding/json" 15) 16 17// ServeSingleActor just simplifies the call from main so 18// that onboarding is as easy as possible 19func ServeSingleActor(actor Actor) { 20 Serve(map[string]Actor{actor.Name: actor}) 21} 22 23// Serve starts an http server with all the required handlers 24func Serve(actors map[string]Actor) { 25 26 var webfingerHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { 27 w.Header().Set("content-type", "application/jrd+json; charset=utf-8") 28 account := r.URL.Query().Get("resource") // should be something like acct:user@example.com 29 account = strings.Replace(account, "acct:", "", 1) // remove acct: 30 server := strings.Split(baseURL, "://")[1] // remove protocol from baseURL. Should get example.com 31 server = strings.TrimSuffix(server, "/") // remove protocol from baseURL. Should get example.com 32 account = strings.Replace(account, "@"+server, "", 1) // remove server from handle. Should get user 33 actor, err := LoadActor(account) 34 // error out if this actor does not exist 35 if err != nil { 36 log.Info("No such actor") 37 w.WriteHeader(http.StatusNotFound) 38 fmt.Fprintf(w, "404 - actor not found") 39 return 40 } 41 // response := `{"subject":"acct:` + actor.name + `@` + server + `","aliases":["` + baseURL + actor.name + `","` + baseURL + actor.name + `"],"links":[{"href":"` + baseURL + `","type":"text/html","rel":"https://webfinger.net/rel/profile-page"},{"href":"` + baseURL + actor.name + `","type":"application/activity+json","rel":"self"}]}` 42 43 responseMap := make(map[string]interface{}) 44 45 responseMap["subject"] = "acct:" + actor.Name + "@" + server 46 // links is a json array with a single element 47 var links [1]map[string]string 48 link1 := make(map[string]string) 49 link1["rel"] = "self" 50 link1["type"] = "application/activity+json" 51 link1["href"] = baseURL + actor.Name 52 links[0] = link1 53 responseMap["links"] = links 54 55 response, err := json.Marshal(responseMap) 56 if err != nil { 57 log.Error("problem creating the webfinger response json") 58 } 59 PrettyPrintJSON(response) 60 w.Write([]byte(response)) 61 } 62 63 var actorHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { 64 w.Header().Set("content-type", "application/activity+json; charset=utf-8") 65 log.Info("Remote server " + r.RemoteAddr + " just fetched our /actor endpoint") 66 username := mux.Vars(r)["actor"] 67 log.Info(username) 68 if username == ".well-known" || username == "favicon.ico" { 69 log.Info("well-known, skipping...") 70 return 71 } 72 actor, err := LoadActor(username) 73 // error out if this actor does not exist (or there are dots or slashes in his name) 74 if err != nil { 75 w.WriteHeader(http.StatusNotFound) 76 fmt.Fprintf(w, "404 - page not found") 77 log.Info("Can't create local actor") 78 return 79 } 80 fmt.Fprintf(w, actor.whoAmI()) 81 82 // Show some debugging information 83 printer.Info("") 84 body, _ := ioutil.ReadAll(r.Body) 85 PrettyPrintJSON(body) 86 log.Info(FormatHeaders(r.Header)) 87 printer.Info("") 88 } 89 90 var outboxHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { 91 w.Header().Set("content-type", "application/activity+json; charset=utf-8") 92 pageStr := r.URL.Query().Get("page") // get the page from the query string as string 93 username := mux.Vars(r)["actor"] // get the needed actor from the muxer (url variable {actor} below) 94 actor, err := LoadActor(username) // load the actor from disk 95 if err != nil { // either actor requested has illegal characters or 96 log.Info("Can't load local actor") // we don't have such actor 97 fmt.Fprintf(w, "404 - page not found") 98 w.WriteHeader(http.StatusNotFound) 99 return 100 } 101 postsPerPage := 100 102 var response []byte 103 filename := storage + slash + "actors" + slash + actor.Name + slash + "outbox.txt" 104 totalLines, err := lineCounter(filename) 105 if err != nil { 106 log.Info("Can't read outbox.txt") 107 log.Info(err) 108 return 109 } 110 if pageStr == "" { 111 //TODO fix total items 112 response = []byte(`{ 113 "@context" : "https://www.w3.org/ns/activitystreams", 114 "first" : "` + baseURL + actor.Name + `/outbox?page=1", 115 "id" : "` + baseURL + actor.Name + `/outbox", 116 "last" : "` + baseURL + actor.Name + `/outbox?page=` + strconv.Itoa(totalLines/postsPerPage+1) + `", 117 "totalItems" : ` + strconv.Itoa(totalLines) + `, 118 "type" : "OrderedCollection" 119 }`) 120 } else { 121 page, err := strconv.Atoi(pageStr) // get page number from query string 122 if err != nil { 123 log.Info("Page number not a number, assuming 1") 124 page = 1 125 } 126 lines, err := ReadLines(filename, (page-1)*postsPerPage, page*(postsPerPage+1)-1) 127 if err != nil { 128 log.Info("Can't read outbox file") 129 log.Info(err) 130 return 131 } 132 responseMap := make(map[string]interface{}) 133 responseMap["@context"] = context() 134 responseMap["id"] = baseURL + actor.Name + "/outbox?page=" + pageStr 135 136 if page*postsPerPage < totalLines { 137 responseMap["next"] = baseURL + actor.Name + "/outbox?page=" + strconv.Itoa(page+1) 138 } 139 if page > 1 { 140 responseMap["prev"] = baseURL + actor.Name + "/outbox?page=" + strconv.Itoa(page-1) 141 } 142 responseMap["partOf"] = baseURL + actor.Name + "/outbox" 143 responseMap["type"] = "OrderedCollectionPage" 144 145 orderedItems := make([]interface{}, 0, postsPerPage) 146 147 for _, item := range lines { 148 // split the line 149 parts := strings.Split(item, "/") 150 151 // keep the hash 152 hash := parts[len(parts)-1] 153 // build the filename 154 filename := storage + slash + "actors" + slash + actor.Name + slash + "items" + slash + hash + ".json" 155 // open the file 156 activityJSON, err := ioutil.ReadFile(filename) 157 if err != nil { 158 log.Error("can't read activity") 159 log.Info(filename) 160 return 161 } 162 var temp map[string]interface{} 163 // put it into a map 164 json.Unmarshal(activityJSON, &temp) 165 // append to orderedItems 166 orderedItems = append(orderedItems, temp) 167 } 168 169 responseMap["orderedItems"] = orderedItems 170 171 response, err = json.Marshal(responseMap) 172 if err != nil { 173 log.Info("can't marshal map to json") 174 log.Info(err) 175 return 176 } 177 } 178 w.Write(response) 179 } 180 181 var inboxHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { 182 b, err := ioutil.ReadAll(r.Body) 183 if err != nil { 184 panic(err) 185 } 186 activity := make(map[string]interface{}) 187 err = json.Unmarshal(b, &activity) 188 if err != nil { 189 log.Error("Probably this request didn't have (valid) JSON inside it") 190 return 191 } 192 // TODO check if it's actually an activity 193 194 // check if case is going to be an issue 195 switch activity["type"] { 196 case "Follow": 197 // load the object as actor 198 actor, err := LoadActor(mux.Vars(r)["actor"]) // load the actor from disk 199 if err != nil { 200 log.Error("No such actor") 201 return 202 } 203 actor.OnFollow(activity) 204 case "Accept": 205 acceptor := activity["actor"].(string) 206 actor, err := LoadActor(mux.Vars(r)["actor"]) // load the actor from disk 207 if err != nil { 208 log.Error("No such actor") 209 return 210 } 211 212 // From here down this could be moved to Actor (TBD) 213 214 follow := activity["object"].(map[string]interface{}) 215 id := follow["id"].(string) 216 217 // check if the object of the follow is us 218 if follow["actor"].(string) != baseURL+actor.Name { 219 log.Info("This is not for us, ignoring") 220 return 221 } 222 // try to get the hash only 223 hash := strings.Replace(id, baseURL+actor.Name+"/item/", "", 1) 224 // if there are still slashes in the result this means the 225 // above didn't work 226 if strings.ContainsAny(hash, "/") { 227 // log.Info(follow) 228 log.Info("The id of this follow is probably wrong") 229 // we could return here but pixelfed returns 230 // the id as http://domain.tld/actor instead of 231 // http://domain.tld/actor/item/hash so this chokes 232 // return 233 } 234 235 // Have we already requested this follow or are we following anybody that 236 // sprays accepts? 237 238 // pixelfed doesn't return the original follow thus the id is wrong so we 239 // need to just check if we requested this actor 240 241 // pixelfed doesn't return the original follow thus the id is wrong so we 242 // need to just check if we requested this actor 243 if _, ok := actor.requested[acceptor]; !ok { 244 log.Info("We never requested this follow from " + acceptor +", ignoring the Accept") 245 return 246 } 247 // if pixelfed fixes https://github.com/pixelfed/pixelfed/issues/1710 we should uncomment 248 // hash is the _ from above 249 250 // if hash != id { 251 // log.Info("Id mismatch between Follow request and Accept") 252 // return 253 // } 254 actor.following[acceptor] = hash 255 PrettyPrint(activity) 256 delete(actor.requested, acceptor) 257 actor.save() 258 case "Reject": 259 rejector := activity["actor"].(string) 260 actor, err := LoadActor(mux.Vars(r)["actor"]) // load the actor from disk 261 if err != nil { 262 log.Error("No such actor") 263 return 264 } 265 // write the actor to the list of rejected follows so that 266 // we won't try following them again 267 actor.rejected[rejector] = "" 268 actor.save() 269 case "Create": 270 actor, ok := actors[mux.Vars(r)["actor"]] // load the actor from memory 271 if !ok { 272 // log.Error(actors) 273 log.Error("No such actor: " + mux.Vars(r)["actor"]) 274 return 275 } 276 log.Info("Received the following activity from: " + r.UserAgent()) 277 PrettyPrintJSON(b) 278 actor.OnReceiveContent(activity) 279 default: 280 281 } 282 } 283 284 var peersHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { 285 w.Header().Set("content-type", "application/activity+json; charset=utf-8") 286 username := mux.Vars(r)["actor"] 287 collection := mux.Vars(r)["peers"] 288 if collection != "followers" && collection != "following" { 289 w.WriteHeader(http.StatusNotFound) 290 w.Write([]byte("404 - No such collection")) 291 return 292 } 293 actor, err := LoadActor(username) 294 // error out if this actor does not exist 295 if err != nil { 296 log.Info("Can't create local actor") 297 return 298 } 299 var page int 300 pageS := r.URL.Query().Get("page") 301 if pageS == "" { 302 page = 0 303 } else { 304 page, _ = strconv.Atoi(pageS) 305 } 306 response, _ := actor.getPeers(page, collection) 307 w.Write(response) 308 } 309 310 var postHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { 311 w.Header().Set("content-type", "application/activity+json; charset=utf-8") 312 username := mux.Vars(r)["actor"] 313 hash := mux.Vars(r)["hash"] 314 actor, err := LoadActor(username) 315 // error out if this actor does not exist 316 if err != nil { 317 log.Info("Can't create local actor") 318 return 319 } 320 post, err := actor.loadItem(hash) 321 if err != nil { 322 w.WriteHeader(http.StatusNotFound) 323 fmt.Fprintf(w, "404 - post not found") 324 return 325 } 326 postJSON, err := json.Marshal(post) 327 if err != nil { 328 log.Info("failed to marshal json from item " + hash + " text") 329 return 330 } 331 w.Write(postJSON) 332 } 333 334 // Add the handlers to a HTTP server 335 gorilla := mux.NewRouter() 336 niCfg := nodeInfoConfig(baseURL) 337 ni := nodeinfo.NewService(*niCfg, nodeInfoResolver{len(actors)}) 338 gorilla.HandleFunc(nodeinfo.NodeInfoPath, http.HandlerFunc(ni.NodeInfoDiscover)) 339 gorilla.HandleFunc(niCfg.InfoURL, http.HandlerFunc(ni.NodeInfo)) 340 gorilla.HandleFunc("/.well-known/webfinger", webfingerHandler) 341 gorilla.HandleFunc("/{actor}/peers/{peers}", peersHandler) 342 gorilla.HandleFunc("/{actor}/outbox", outboxHandler) 343 gorilla.HandleFunc("/{actor}/outbox/", outboxHandler) 344 gorilla.HandleFunc("/{actor}/inbox", inboxHandler) 345 gorilla.HandleFunc("/{actor}/inbox/", inboxHandler) 346 gorilla.HandleFunc("/{actor}/", actorHandler) 347 gorilla.HandleFunc("/{actor}", actorHandler) 348 gorilla.HandleFunc("/{actor}/item/{hash}", postHandler) 349 http.Handle("/", gorilla) 350 351 log.Fatal(http.ListenAndServe(":8081", nil)) 352} 353