diff --git a/TODO b/TODO index 4be5df8..9f11405 100644 --- a/TODO +++ b/TODO @@ -1,8 +1,3 @@ -[ ] Load outbox of users and parse latest posts -[ ] Write these posts to local file - (normally we would need to sort by time but this is a - temporary solution until we really follow the actors - and get notifications in timely manner) [ ] Follow users [ ] Announcements (read up how boost json looks like) [ ] Federate the post to our followers (hardcoded for now) @@ -13,14 +8,14 @@ [ ] Handle the /actor endpoint [ ] Create configuration file [ ] Implement database backend - [ ] Create a file with the actors we have, their following + [✔] Create a file with the actors we have, their following and their followers. - [ ] `MakeActor` should create a file with that actor. - [ ] Implement `LoadActor` + [✔] `MakeActor` should create a file with that actor. + [✔] Implement `LoadActor` [ ] All but `main.go` should run LoadActor instead of MakeActor (Actually nobody should run LoadActor except GetActor) [ ] `actor.Follow` should write the new following to file - [ ] Handle being followed + [✔] Handle being followed [ ] When followed, the handler should write the new follower to file [ ] Make sure we send our boosts to all our followers Code is there but it works sometimes (I hate when this happens) diff --git a/activityserve/actor.go b/activityserve/actor.go index faacafc..117644d 100644 --- a/activityserve/actor.go +++ b/activityserve/actor.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "os" + "strconv" "strings" "time" @@ -57,7 +58,7 @@ func MakeActor(name, summary, actorType string) (Actor, error) { following := make(map[string]interface{}) followersIRI := baseURL + name + "/followers" publicKeyID := baseURL + name + "#main-key" - iri := baseURL + "/" + name + iri := baseURL + name nuIri, err := url.Parse(iri) if err != nil { log.Info("Something went wrong when parsing the local actor uri into net/url") @@ -193,6 +194,10 @@ func LoadActor(name string) (Actor, error) { return actor, nil } +// func LoadActorFromIRI(iri string) a Actor{ + +// } + // save the actor to file func (a *Actor) save() error { @@ -250,20 +255,22 @@ func (a *Actor) whoAmI() string { }` } -func (a *Actor) newID() string { +func (a *Actor) newIDhash() string { return uniuri.New() } +func (a *Actor) newIDurl() string { + return baseURL + a.name + "/" + a.newIDhash() +} + // CreateNote posts an activityPub note to our followers func (a *Actor) CreateNote(content string) { // for now I will just write this to the outbox - id := a.newID() + id := a.newIDurl() create := make(map[string]interface{}) note := make(map[string]interface{}) - context := make([]string, 1) - context[0] = "https://www.w3.org/ns/activitystreams" - create["@context"] = context + create["@context"] = context() create["actor"] = baseURL + a.name create["cc"] = a.followersIRI create["id"] = baseURL + a.name + "/" + id @@ -279,34 +286,6 @@ func (a *Actor) CreateNote(content string) { note["to"] = "https://www.w3.org/ns/activitystreams#Public" create["published"] = note["published"] create["type"] = "Create" - - // note := `{ - // "actor" : "https://` + baseURL + a.name + `", - // "cc" : [ - // "https://` + baseURL + a.name + `/followers" - // ], - // "id" : "https://` + baseURL + a.name + `/` + id +`", - // "object" : { - // "attributedTo" : "https://` + baseURL + a.name + `", - // "cc" : [ - // "https://` + baseURL + a.name + `/followers" - // ], - // "content" : "`+ content + `", - // "id" : "https://` + baseURL + a.name + `/` + id +`", - // "inReplyTo" : null, - // "published" : "2019-08-26T16:25:26Z", - // "to" : [ - // "https://www.w3.org/ns/activitystreams#Public" - // ], - // "type" : "Note", - // "url" : "https://` + baseURL + a.name + `/` + id +`" - // }, - // "published" : "2019-08-26T16:25:26Z", - // "to" : [ - // "https://www.w3.org/ns/activitystreams#Public" - // ], - // "type" : "Create" - // }` to, _ := url.Parse("https://cybre.space/inbox") go a.send(create, to) a.saveItem(id, create) @@ -330,6 +309,33 @@ func (a *Actor) send(content map[string]interface{}, to *url.URL) (err error) { return a.signedHTTPPost(content, to.String()) } +// GetFollowers returns a list of people that follow us +func (a *Actor) GetFollowers(page int) (response []byte, err error) { + // if there's no page parameter mastodon displays an + // OrderedCollection with info of where to find orderedCollectionPages + // with the actual information. We are mirroring that behavior + themap := make(map[string]interface{}) + themap["@context"] = "https://www.w3.org/ns/activitystreams" + if page == 0 { + themap["first"] = baseURL + a.name + "/followers?page=1" + themap["id"] = baseURL + a.name + "/followers" + themap["totalItems"] = strconv.Itoa(len(a.followers)) + themap["type"] = "OrderedCollection" + } else if page == 1 { // implement pagination + themap["id"] = baseURL + a.name + "followers?page=" + strconv.Itoa(page) + items := make([]string, 0, len(a.followers)) + for k := range a.followers { + items = append(items, k) + } + themap["orderedItems"] = items + themap["partOf"] = baseURL + a.name + "/followers" + themap["totalItems"] = len(a.followers) + themap["type"] = "OrderedCollectionPage" + } + response, _ = json.Marshal(themap) + return +} + func (a *Actor) signedHTTPPost(content map[string]interface{}, to string) (err error) { b, err := json.Marshal(content) if err != nil { @@ -357,7 +363,7 @@ func (a *Actor) signedHTTPPost(content map[string]interface{}, to string) (err e } req.Header.Add("Accept-Charset", "utf-8") req.Header.Add("Date", time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT") - req.Header.Add("User-Agent", fmt.Sprintf("activityserve 0.0")) + req.Header.Add("User-Agent", userAgent + " " + version) req.Header.Add("Host", iri.Host) req.Header.Add("Accept", "application/activity+json") sum := sha256.Sum256(b) @@ -377,16 +383,16 @@ func (a *Actor) signedHTTPPost(content map[string]interface{}, to string) (err e defer resp.Body.Close() if !isSuccess(resp.StatusCode) { responseData, _ := ioutil.ReadAll(resp.Body) - err = fmt.Errorf("POST request to %s failed (%d): %s\nResponse: %s \nRequest: %s \nHeaders: %s", to, resp.StatusCode, resp.Status, formatJSON(responseData), formatJSON(byteCopy), req.Header) + err = fmt.Errorf("POST request to %s failed (%d): %s\nResponse: %s \nRequest: %s \nHeaders: %s", to, resp.StatusCode, resp.Status, FormatJSON(responseData), FormatJSON(byteCopy), FormatHeaders(req.Header)) log.Info(err) return } responseData, _ := ioutil.ReadAll(resp.Body) - fmt.Printf("POST request to %s succeeded (%d): %s \nResponse: %s \nRequest: %s \nHeaders: %s", to, resp.StatusCode, resp.Status, formatJSON(responseData), formatJSON(byteCopy), req.Header) + fmt.Printf("POST request to %s succeeded (%d): %s \nResponse: %s \nRequest: %s \nHeaders: %s", to, resp.StatusCode, resp.Status, FormatJSON(responseData), FormatJSON(byteCopy), FormatHeaders(req.Header)) return } -func (a *Actor) signedHTTPGet(address string) (string, error){ +func (a *Actor) signedHTTPGet(address string) (string, error) { req, err := http.NewRequest("GET", address, nil) if err != nil { log.Error("cannot create new http.request") @@ -413,7 +419,7 @@ func (a *Actor) signedHTTPGet(address string) (string, error){ log.Error("Can't sign the request") return "", err } - + resp, err := client.Do(req) if err != nil { log.Error("Cannot perform the GET request") @@ -422,14 +428,20 @@ func (a *Actor) signedHTTPGet(address string) (string, error){ } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - + responseData, _ := ioutil.ReadAll(resp.Body) - return "", fmt.Errorf("GET request to %s failed (%d): %s \n%s", iri.String(), resp.StatusCode, resp.Status, formatJSON(responseData)) + return "", fmt.Errorf("GET request to %s failed (%d): %s \n%s", iri.String(), resp.StatusCode, resp.Status, FormatJSON(responseData)) } responseData, _ := ioutil.ReadAll(resp.Body) - fmt.Println("GET request succeeded:", iri.String(), req.Header, resp.StatusCode, resp.Status, "\n", formatJSON(responseData)) + fmt.Println("GET request succeeded:", iri.String(), req.Header, resp.StatusCode, resp.Status, "\n", FormatJSON(responseData)) responseText := string(responseData) return responseText, nil -} \ No newline at end of file +} + +// NewFollower records a new follower to the actor file +func (a *Actor) NewFollower(iri string) error { + a.followers[iri] = struct{}{} + return a.save() +} diff --git a/activityserve/http.go b/activityserve/http.go index daee436..396786e 100644 --- a/activityserve/http.go +++ b/activityserve/http.go @@ -2,7 +2,9 @@ package activityserve import ( "fmt" + "io/ioutil" "net/http" + "strconv" "strings" "github.com/gologme/log" @@ -11,7 +13,7 @@ import ( "encoding/json" ) -// SetupHTTP starts an http server with all the required handlers +// Serve starts an http server with all the required handlers func Serve() { var webfingerHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { @@ -34,23 +36,26 @@ func Serve() { responseMap := make(map[string]interface{}) responseMap["subject"] = "acct:" + actor.name + "@" + server - links := make(map[string]string) - links["rel"] = "self" - links["type"] = "application/activity+json" - links["href"] = baseURL + actor.name + // 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"] = "application/activity+json" + link1["href"] = baseURL + actor.name + links[0] = link1 responseMap["links"] = links response, err := json.Marshal(responseMap) if err != nil { log.Error("problem creating the webfinger response json") } - log.Info(string(response)) + PrettyPrintJSON(response) w.Write([]byte(response)) } var actorHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { w.Header().Set("content-type", "application/activity+json; charset=utf-8") - log.Info("Remote server just fetched our /actor endpoint") + log.Info("Remote server " + r.RemoteAddr + " just fetched our /actor endpoint") username := mux.Vars(r)["actor"] log.Info(username) if username == ".well-known" || username == "favicon.ico" { @@ -66,9 +71,13 @@ func Serve() { return } fmt.Fprintf(w, actor.whoAmI()) - log.Info(r.RemoteAddr) - log.Info(r.Body) - log.Info(r.Header) + + // Show some debugging information + printer.Info("") + body, _ := ioutil.ReadAll(r.Body) + PrettyPrintJSON(body) + log.Info(FormatHeaders(r.Header)) + printer.Info("") } var outboxHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { @@ -137,13 +146,101 @@ func Serve() { w.Write([]byte(response)) } + var inboxHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { + b, err := ioutil.ReadAll(r.Body) + if err != nil { + panic(err) + } + activity := make(map[string]interface{}) + err = json.Unmarshal(b, &activity) + if err != nil { + log.Error("Probably this request didn't have (valid) JSON inside it") + return + } + // TODO check if it's actually an activity + + // check if case is going to be an issue + switch activity["type"] { + case "Follow": + // it's a follow, write it down + newFollower := activity["actor"].(string) + // check we aren't following ourselves + if newFollower == activity["object"] { + log.Info("You can't follow yourself") + return + } + // load the object as actor + actor, err := LoadActor(mux.Vars(r)["actor"]) // load the actor from disk + if err != nil { + log.Error("No such actor") + return + } + + // check if this user is already following us + if _, ok := actor.followers[newFollower]; ok { + log.Info("You're already following us, yay!") + // do nothing, they're already following us + } else { + actor.NewFollower(newFollower) + } + // send accept anyway even if they are following us already + // this is very verbose. I would prefer creating a map by hand + + // remove @context from the inner activity + delete(activity, "@context") + + accept := make(map[string]interface{}) + + accept["@context"] = "https://www.w3.org/ns/activitystreams" + accept["to"] = activity["actor"] + accept["id"] = actor.newIDurl() + accept["actor"] = actor.iri + accept["object"] = activity + accept["type"] = "Accept" + + follower, err := NewRemoteActor(activity["actor"].(string)) + + if err != nil { + log.Info("Couldn't retrieve remote actor info, maybe server is down?") + log.Info(err) + } + + go actor.signedHTTPPost(accept, follower.inbox) + + default: + + } + + } + + var followersHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/activity+json; charset=utf-8") + username := mux.Vars(r)["actor"] + actor, err := LoadActor(username) + // error out if this actor does not exist + if err != nil { + log.Info("Can't create local actor") + return + } + var page int + pageS := r.URL.Query().Get("page") + if pageS == "" { + page = 0 + } else { + page, _ = strconv.Atoi(pageS) + } + response, _ := actor.GetFollowers(page) + w.Write(response) + } + // Add the handlers to a HTTP server gorilla := mux.NewRouter() gorilla.HandleFunc("/.well-known/webfinger", webfingerHandler) + gorilla.HandleFunc("/{actor}/followers", followersHandler) gorilla.HandleFunc("/{actor}/outbox", outboxHandler) gorilla.HandleFunc("/{actor}/outbox/", outboxHandler) - // gorilla.HandleFunc("/{actor}/inbox", inboxHandler) - // gorilla.HandleFunc("/{actor}/inbox/", inboxHandler) + gorilla.HandleFunc("/{actor}/inbox", inboxHandler) + gorilla.HandleFunc("/{actor}/inbox/", inboxHandler) gorilla.HandleFunc("/{actor}/", actorHandler) gorilla.HandleFunc("/{actor}", actorHandler) // gorilla.HandleFunc("/{actor}/post/{hash}", postHandler) diff --git a/activityserve/remoteActor.go b/activityserve/remoteActor.go new file mode 100644 index 0000000..b5f96a1 --- /dev/null +++ b/activityserve/remoteActor.go @@ -0,0 +1,92 @@ +package activityserve + +import ( + "fmt" + "io/ioutil" + + "github.com/gologme/log" + + // "github.com/go-fed/activity/pub" + // "github.com/go-fed/httpsig" + + "net/http" + // "net/url" + + "encoding/json" + + "bytes" +) + +// RemoteActor is a type that holds an actor +// that we want to interact with +type RemoteActor struct { + iri, outbox, inbox string + info map[string]interface{} +} + +// NewRemoteActor returns a remoteActor which holds +// all the info required for an actor we want to +// interact with (not essentially sitting in our instance) +func NewRemoteActor(iri string) (RemoteActor, error) { + + info, err := get(iri) + if err != nil { + log.Info("Couldn't get remote actor information") + log.Info(err) + return RemoteActor{}, err + } + + outbox := info["outbox"].(string) + inbox := info["inbox"].(string) + + return RemoteActor{ + iri: iri, + outbox: outbox, + inbox: inbox, + }, err +} + +func (ra RemoteActor) getLatestPosts(number int) (map[string]interface{}, error) { + return get(ra.outbox) +} + +func get(iri string) (info map[string]interface{}, err error) { + + buf := new(bytes.Buffer) + + req, err := http.NewRequest("GET", iri, buf) + if err != nil { + log.Info(err) + return + } + req.Header.Add("Accept", "application/activity+json; profile=\"https://www.w3.org/ns/activitystreams\"") + req.Header.Add("User-Agent", userAgent+" "+version) + req.Header.Add("Accept-Charset", "utf-8") + + resp, err := client.Do(req) + + if err != nil { + log.Info("Cannot perform the request") + log.Info(err) + return + } + + responseData, _ := ioutil.ReadAll(resp.Body) + + if !isSuccess(resp.StatusCode) { + err = fmt.Errorf("GET request to %s failed (%d): %s\nResponse: %s \nHeaders: %s", iri, resp.StatusCode, resp.Status, FormatJSON(responseData), FormatHeaders(req.Header)) + log.Info(err) + return + } + + var e interface{} + err = json.Unmarshal(responseData, &e) + + if err != nil { + log.Info("something went wrong when unmarshalling the json") + log.Info(err) + } + info = e.(map[string]interface{}) + + return +} diff --git a/activityserve/setup.go b/activityserve/setup.go index 267c810..be3dbe2 100644 --- a/activityserve/setup.go +++ b/activityserve/setup.go @@ -2,8 +2,8 @@ package activityserve import ( "fmt" - "os" "net/http" + "os" "github.com/gologme/log" "gopkg.in/ini.v1" @@ -13,6 +13,7 @@ var slash = string(os.PathSeparator) var baseURL = "http://example.com/" var storage = "storage" var userAgent = "activityserve" +var printer *log.Logger const libName = "activityserve" const version = "0.99" @@ -61,7 +62,7 @@ func Setup(configurationFile string, debug bool) { log.EnableLevel("warn") // create a logger with levels but without prefixes for easier to read // debug output - printer := log.New(os.Stdout, " ", 0) + printer = log.New(os.Stdout, " ", 0) if debug == true { fmt.Println() diff --git a/activityserve/snips.md b/activityserve/snips.md new file mode 100644 index 0000000..6d53527 --- /dev/null +++ b/activityserve/snips.md @@ -0,0 +1,86 @@ +## When we follow someone from pherephone 1.00 + +``` json + +{ + "@context": "https://www.w3.org/ns/activitystreams", + "actor": "https://floorb.qwazix.com/myAwesomeList1", + "id": "https://floorb.qwazix.com/myAwesomeList1/Xm9UHyJXyFYduqXz", + "object": "https://cybre.space/users/qwazix", + "to": "https://cybre.space/users/qwazix", + "type": "Follow" +} +``` + +``` yaml + +Accept: application/activity+json +Accept-Charset: utf-8 +Date: Tue, 10 Sep 2019 05:31:22 GMT +Digest: SHA-256=uL1LvGU4+gSDm8Qci6XibZODTaNCsXWXWgkMWAqBvG8= +Host: cybre.space +Signature: keyId="https://floorb.qwazix.com/myAwesomeList1#main-key",algorithm="rsa-sha256",headers="(request-target) date host digest",signature="c6oipeXu/2zqX3qZF1x7KLNTYifcyqwwDySoslAowjpYlKWO3qAZMU1A//trYm23AtnItXkH2mY3tPq8X7fy9P1+CMFmiTzV01MGwwwJLDtEXKoq8W7L7lWuQhDD5rjiZqWyei4T13FW7MOCRbAtC4kZqkHrp5Z3l8HhPvmgUV5VOuSGWrtbmCN3hlAEHVugQTMPC6UjlaHva6Qm/SNlFmpUdG7WmUUPJIZ6a/ysBk4cLkF1+Hb03grXKexLHAU4bPIRcjwFpUl06yp8fZ8CCLhNhIsBACiizV85D3votmdxAollE5JXSwBp4f6jrZbgiJEusFoxiVKKqZRHRESQBQ==" + +``` + +## Pherephone 1 Accept Activity + +``` yaml + Accept: application/activity+json + Accept-Charset: utf-8 + Date: Tue, 10 Sep 2019 07:28:49 GMT + Digest: SHA-256=GTy9bhYjOnbeCJzAzpqI/HEw/5p81NnoPLJkVAiZ4K0= + Host: cybre.space + Signature: keyId="https://floorb.qwazix.com/activityserve_test_actor_1#main-key",algorithm="rsa-sha256",headers="(request-target) date host digest",signature="jAeTEy9v1t+bCwQJB2R4Cscu/fGu5i4luHXlzJcJVyRbsHGqxbNEOxlk/G0S5BGbX3Kuoerq2oMpkFV5kCWPlpAmfhz38NKIrWhjnEUpFOfiG+ZJBpQsb3VQp7M3RGPZ9K4hmV6BSzkC8npsFGPI/HkAaj9u/txW5Cp4v6dMOYteoRLcKc3UVPK9j4hCbjq6SPhpwfM+StARSDnUFfpDe4YYQiVnO2WoINPUr4xvELmCYdBclSBCKcG66g8sBpnx4McjIlu0VISeBxzIHZYOONPteLY2uZW3Axi9JIAq88Y2Ecw4vV6Ctp7KcmD7M3kAJLqao2p/XZNZ3ExsTGfrXA==" + User-Agent: activityserve 0.0 +``` + +``` json +{ + "@context": "https://www.w3.org/ns/activitystreams", + "actor": "https://floorb.qwazix.com/myAwesomeList1", + "id": "https://floorb.qwazix.com/myAwesomeList1/SABRE7xlDAjtDcZb", + "object": { + "actor": "https://cybre.space/users/qwazix", + "id": "https://cybre.space/3e7336af-4bcd-4f77-aa69-6a145be824aa", + "object": "https://floorb.qwazix.com/myAwesomeList1", + "type": "Follow" + }, + "to": "https://cybre.space/users/qwazix", + "type": "Accept" +} +``` + +## Pherephone 2 Accept Activity + +``` yaml + +Accept: application/activity+json +Accept-Charset: utf-8 +Date: Tue, 10 Sep 2019 07:32:08 GMT +Digest: SHA-256=yKzA6srSMx0b5GXn9DyflXVdqWd6ADBGt5hO9t/yc44= +Host: cybre.space +Signature: keyId="https://floorb.qwazix.com/myAwesomeList1#main-key",algorithm="rsa-sha256",headers="(request-target) date host digest",signature="WERXWDRFS7aGiIoz+HSujtuv9XNFBPxHkJSsCPu7PNIUDoAB2jdwW3rZc5jbrSLxi9Aqhr2BiBV/VYELQ8gITPzzIYH5sizPcPyLyARPUw37t6zA3HinahpfBKXhf73q9u+CYE/7DMKQ2Pvv2lQPaZ8hl27R2KJmcc3Jhmn5nxrQ+kxAtn6qYpNT/BqLWlXKx5rpYM2r+mHjFyYRYsjlAmi+RQNDEmv/uwn+XuNKzEtrL8Oq7mM13Lsid0a3gJi/t0b/luoyRyvi3fHUM/b1epfVogG/FulsZ0A92310v8MbastceQjjUzTzjKHILl7qNewkqtlzn2ARm3cZlAprSg==" +User-Agent: pherephone (go-fed/activity v1.0.0) + + +``` + +``` json + +{ + "@context": "https://www.w3.org/ns/activitystreams", + "actor": "https://floorb.qwazix.com/activityserve_test_actor_1", + "id": "https://floorb.qwazix.com/activityserve_test_actor_1/4wJ9DrBab4eIE3Bt", + "object": { + "actor": "https://cybre.space/users/qwazix", + "id": "https://cybre.space/9123da78-21a5-44bc-bce5-4039a4072e4c", + "object": "https://floorb.qwazix.com/activityserve_test_actor_1", + "type": "Follow" + }, + "to": "https://cybre.space/users/qwazix", + "type": "Accept" +} + +``` + diff --git a/activityserve/util.go b/activityserve/util.go index ba937a6..f13f501 100644 --- a/activityserve/util.go +++ b/activityserve/util.go @@ -34,8 +34,19 @@ func PrettyPrintJSON(theJSON []byte) { log.Info(dst) } -func formatJSON(theJSON []byte) string{ +func FormatJSON(theJSON []byte) string { dst := new(bytes.Buffer) json.Indent(dst, theJSON, "", "\t") return dst.String() } + +// FormatHeaders to string for printing +func FormatHeaders(header http.Header) string { + buf := new(bytes.Buffer) + header.Write(buf) + return buf.String() +} + +func context() [1]string { + return [1]string{"https://www.w3.org/ns/activitystreams"} +} diff --git a/main.go b/main.go index 536f5be..53056fc 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,9 @@ package main import ( - "fmt" "flag" + "fmt" + // "os" // "strings" @@ -26,7 +27,6 @@ import ( "./activityserve" ) - var err error func main() { @@ -51,9 +51,11 @@ func main() { activityserve.Setup("config.ini", *debugFlag) - actor, _ := activityserve.MakeActor("activityserve_test_actor_2", "This is an activityserve test actor", "Service") + // actor, _ := activityserve.MakeActor("activityserve_test_actor_2", "This is an activityserve test actor", "Service") // actor, _ := activityserve.LoadActor("activityserve_test_actor_2") - actor.CreateNote("Hello World!") + // actor.CreateNote("Hello World!") + + activityserve.LoadActor("activityserve_test_actor_2") activityserve.Serve() -} \ No newline at end of file +}