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