A clean, Markdown-based publishing platform made for writers. Write together, and build a community. https://writefreely.org
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

746 lines
19 KiB

  1. /*
  2. * Copyright © 2018-2019 A Bunch Tell LLC.
  3. *
  4. * This file is part of WriteFreely.
  5. *
  6. * WriteFreely is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License, included
  8. * in the LICENSE file in this source code package.
  9. */
  10. package writefreely
  11. import (
  12. "bytes"
  13. "crypto/sha256"
  14. "database/sql"
  15. "encoding/base64"
  16. "encoding/json"
  17. "fmt"
  18. "io/ioutil"
  19. "net/http"
  20. "net/http/httputil"
  21. "net/url"
  22. "strconv"
  23. "time"
  24. "github.com/gorilla/mux"
  25. "github.com/writeas/activity/streams"
  26. "github.com/writeas/httpsig"
  27. "github.com/writeas/impart"
  28. "github.com/writeas/nerds/store"
  29. "github.com/writeas/web-core/activitypub"
  30. "github.com/writeas/web-core/activitystreams"
  31. "github.com/writeas/web-core/log"
  32. )
  33. const (
  34. // TODO: delete. don't use this!
  35. apCustomHandleDefault = "blog"
  36. )
  37. type RemoteUser struct {
  38. ID int64
  39. ActorID string
  40. Inbox string
  41. SharedInbox string
  42. }
  43. func (ru *RemoteUser) AsPerson() *activitystreams.Person {
  44. return &activitystreams.Person{
  45. BaseObject: activitystreams.BaseObject{
  46. Type: "Person",
  47. Context: []interface{}{
  48. activitystreams.Namespace,
  49. },
  50. ID: ru.ActorID,
  51. },
  52. Inbox: ru.Inbox,
  53. Endpoints: activitystreams.Endpoints{
  54. SharedInbox: ru.SharedInbox,
  55. },
  56. }
  57. }
  58. func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Request) error {
  59. w.Header().Set("Server", serverSoftware)
  60. vars := mux.Vars(r)
  61. alias := vars["alias"]
  62. // TODO: enforce visibility
  63. // Get base Collection data
  64. var c *Collection
  65. var err error
  66. if app.cfg.App.SingleUser {
  67. c, err = app.db.GetCollectionByID(1)
  68. } else {
  69. c, err = app.db.GetCollection(alias)
  70. }
  71. if err != nil {
  72. return err
  73. }
  74. suspended, err := app.db.IsUserSuspended(c.OwnerID)
  75. if err != nil {
  76. log.Error("fetch collection inbox: get owner: %v", err)
  77. return ErrInternalGeneral
  78. }
  79. if suspended {
  80. return ErrCollectionNotFound
  81. }
  82. c.hostName = app.cfg.App.Host
  83. p := c.PersonObject()
  84. return impart.RenderActivityJSON(w, p, http.StatusOK)
  85. }
  86. func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Request) error {
  87. w.Header().Set("Server", serverSoftware)
  88. vars := mux.Vars(r)
  89. alias := vars["alias"]
  90. // TODO: enforce visibility
  91. // Get base Collection data
  92. var c *Collection
  93. var err error
  94. if app.cfg.App.SingleUser {
  95. c, err = app.db.GetCollectionByID(1)
  96. } else {
  97. c, err = app.db.GetCollection(alias)
  98. }
  99. if err != nil {
  100. return err
  101. }
  102. suspended, err := app.db.IsUserSuspended(c.OwnerID)
  103. if err != nil {
  104. log.Error("fetch collection inbox: get owner: %v", err)
  105. return ErrInternalGeneral
  106. }
  107. if suspended {
  108. return ErrCollectionNotFound
  109. }
  110. c.hostName = app.cfg.App.Host
  111. if app.cfg.App.SingleUser {
  112. if alias != c.Alias {
  113. return ErrCollectionNotFound
  114. }
  115. }
  116. res := &CollectionObj{Collection: *c}
  117. app.db.GetPostsCount(res, false)
  118. accountRoot := c.FederatedAccount()
  119. page := r.FormValue("page")
  120. p, err := strconv.Atoi(page)
  121. if err != nil || p < 1 {
  122. // Return outbox
  123. oc := activitystreams.NewOrderedCollection(accountRoot, "outbox", res.TotalPosts)
  124. return impart.RenderActivityJSON(w, oc, http.StatusOK)
  125. }
  126. // Return outbox page
  127. ocp := activitystreams.NewOrderedCollectionPage(accountRoot, "outbox", res.TotalPosts, p)
  128. ocp.OrderedItems = []interface{}{}
  129. posts, err := app.db.GetPosts(app.cfg, c, p, false, true, false)
  130. for _, pp := range *posts {
  131. pp.Collection = res
  132. o := pp.ActivityObject(app.cfg)
  133. a := activitystreams.NewCreateActivity(o)
  134. ocp.OrderedItems = append(ocp.OrderedItems, *a)
  135. }
  136. return impart.RenderActivityJSON(w, ocp, http.StatusOK)
  137. }
  138. func handleFetchCollectionFollowers(app *App, w http.ResponseWriter, r *http.Request) error {
  139. w.Header().Set("Server", serverSoftware)
  140. vars := mux.Vars(r)
  141. alias := vars["alias"]
  142. // TODO: enforce visibility
  143. // Get base Collection data
  144. var c *Collection
  145. var err error
  146. if app.cfg.App.SingleUser {
  147. c, err = app.db.GetCollectionByID(1)
  148. } else {
  149. c, err = app.db.GetCollection(alias)
  150. }
  151. if err != nil {
  152. return err
  153. }
  154. suspended, err := app.db.IsUserSuspended(c.OwnerID)
  155. if err != nil {
  156. log.Error("fetch collection inbox: get owner: %v", err)
  157. return ErrInternalGeneral
  158. }
  159. if suspended {
  160. return ErrCollectionNotFound
  161. }
  162. c.hostName = app.cfg.App.Host
  163. accountRoot := c.FederatedAccount()
  164. folls, err := app.db.GetAPFollowers(c)
  165. if err != nil {
  166. return err
  167. }
  168. page := r.FormValue("page")
  169. p, err := strconv.Atoi(page)
  170. if err != nil || p < 1 {
  171. // Return outbox
  172. oc := activitystreams.NewOrderedCollection(accountRoot, "followers", len(*folls))
  173. return impart.RenderActivityJSON(w, oc, http.StatusOK)
  174. }
  175. // Return outbox page
  176. ocp := activitystreams.NewOrderedCollectionPage(accountRoot, "followers", len(*folls), p)
  177. ocp.OrderedItems = []interface{}{}
  178. /*
  179. for _, f := range *folls {
  180. ocp.OrderedItems = append(ocp.OrderedItems, f.ActorID)
  181. }
  182. */
  183. return impart.RenderActivityJSON(w, ocp, http.StatusOK)
  184. }
  185. func handleFetchCollectionFollowing(app *App, w http.ResponseWriter, r *http.Request) error {
  186. w.Header().Set("Server", serverSoftware)
  187. vars := mux.Vars(r)
  188. alias := vars["alias"]
  189. // TODO: enforce visibility
  190. // Get base Collection data
  191. var c *Collection
  192. var err error
  193. if app.cfg.App.SingleUser {
  194. c, err = app.db.GetCollectionByID(1)
  195. } else {
  196. c, err = app.db.GetCollection(alias)
  197. }
  198. if err != nil {
  199. return err
  200. }
  201. suspended, err := app.db.IsUserSuspended(c.OwnerID)
  202. if err != nil {
  203. log.Error("fetch collection inbox: get owner: %v", err)
  204. return ErrInternalGeneral
  205. }
  206. if suspended {
  207. return ErrCollectionNotFound
  208. }
  209. c.hostName = app.cfg.App.Host
  210. accountRoot := c.FederatedAccount()
  211. page := r.FormValue("page")
  212. p, err := strconv.Atoi(page)
  213. if err != nil || p < 1 {
  214. // Return outbox
  215. oc := activitystreams.NewOrderedCollection(accountRoot, "following", 0)
  216. return impart.RenderActivityJSON(w, oc, http.StatusOK)
  217. }
  218. // Return outbox page
  219. ocp := activitystreams.NewOrderedCollectionPage(accountRoot, "following", 0, p)
  220. ocp.OrderedItems = []interface{}{}
  221. return impart.RenderActivityJSON(w, ocp, http.StatusOK)
  222. }
  223. func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request) error {
  224. w.Header().Set("Server", serverSoftware)
  225. vars := mux.Vars(r)
  226. alias := vars["alias"]
  227. var c *Collection
  228. var err error
  229. if app.cfg.App.SingleUser {
  230. c, err = app.db.GetCollectionByID(1)
  231. } else {
  232. c, err = app.db.GetCollection(alias)
  233. }
  234. if err != nil {
  235. // TODO: return Reject?
  236. return err
  237. }
  238. suspended, err := app.db.IsUserSuspended(c.OwnerID)
  239. if err != nil {
  240. log.Error("fetch collection inbox: get owner: %v", err)
  241. return ErrInternalGeneral
  242. }
  243. if suspended {
  244. return ErrCollectionNotFound
  245. }
  246. c.hostName = app.cfg.App.Host
  247. if debugging {
  248. dump, err := httputil.DumpRequest(r, true)
  249. if err != nil {
  250. log.Error("Can't dump: %v", err)
  251. } else {
  252. log.Info("Rec'd! %q", dump)
  253. }
  254. }
  255. var m map[string]interface{}
  256. if err := json.NewDecoder(r.Body).Decode(&m); err != nil {
  257. return err
  258. }
  259. a := streams.NewAccept()
  260. p := c.PersonObject()
  261. var to *url.URL
  262. var isFollow, isUnfollow bool
  263. fullActor := &activitystreams.Person{}
  264. var remoteUser *RemoteUser
  265. res := &streams.Resolver{
  266. FollowCallback: func(f *streams.Follow) error {
  267. isFollow = true
  268. // 1) Use the Follow concrete type here
  269. // 2) Errors are propagated to res.Deserialize call below
  270. m["@context"] = []string{activitystreams.Namespace}
  271. b, _ := json.Marshal(m)
  272. if debugging {
  273. log.Info("Follow: %s", b)
  274. }
  275. _, followID := f.GetId()
  276. if followID == nil {
  277. log.Error("Didn't resolve follow ID")
  278. } else {
  279. aID := c.FederatedAccount() + "#accept-" + store.GenerateFriendlyRandomString(20)
  280. acceptID, err := url.Parse(aID)
  281. if err != nil {
  282. log.Error("Couldn't parse generated Accept URL '%s': %v", aID, err)
  283. }
  284. a.SetId(acceptID)
  285. }
  286. a.AppendObject(f.Raw())
  287. _, to = f.GetActor(0)
  288. obj := f.Raw().GetObjectIRI(0)
  289. a.AppendActor(obj)
  290. // First get actor information
  291. if to == nil {
  292. return fmt.Errorf("No valid `to` string")
  293. }
  294. fullActor, remoteUser, err = getActor(app, to.String())
  295. if err != nil {
  296. return err
  297. }
  298. return impart.RenderActivityJSON(w, m, http.StatusOK)
  299. },
  300. UndoCallback: func(u *streams.Undo) error {
  301. isUnfollow = true
  302. m["@context"] = []string{activitystreams.Namespace}
  303. b, _ := json.Marshal(m)
  304. if debugging {
  305. log.Info("Undo: %s", b)
  306. }
  307. a.AppendObject(u.Raw())
  308. _, to = u.GetActor(0)
  309. // TODO: get actor from object.object, not object
  310. obj := u.Raw().GetObjectIRI(0)
  311. a.AppendActor(obj)
  312. if to != nil {
  313. // Populate fullActor from DB?
  314. remoteUser, err = getRemoteUser(app, to.String())
  315. if err != nil {
  316. if iErr, ok := err.(*impart.HTTPError); ok {
  317. if iErr.Status == http.StatusNotFound {
  318. log.Error("No remoteuser info for Undo event!")
  319. }
  320. }
  321. return err
  322. } else {
  323. fullActor = remoteUser.AsPerson()
  324. }
  325. } else {
  326. log.Error("No to on Undo!")
  327. }
  328. return impart.RenderActivityJSON(w, m, http.StatusOK)
  329. },
  330. }
  331. if err := res.Deserialize(m); err != nil {
  332. // 3) Any errors from #2 can be handled, or the payload is an unknown type.
  333. log.Error("Unable to resolve Follow: %v", err)
  334. if debugging {
  335. log.Error("Map: %s", m)
  336. }
  337. return err
  338. }
  339. go func() {
  340. time.Sleep(2 * time.Second)
  341. am, err := a.Serialize()
  342. if err != nil {
  343. log.Error("Unable to serialize Accept: %v", err)
  344. return
  345. }
  346. am["@context"] = []string{activitystreams.Namespace}
  347. if to == nil {
  348. log.Error("No to! %v", err)
  349. return
  350. }
  351. err = makeActivityPost(app.cfg.App.Host, p, fullActor.Inbox, am)
  352. if err != nil {
  353. log.Error("Unable to make activity POST: %v", err)
  354. return
  355. }
  356. if isFollow {
  357. t, err := app.db.Begin()
  358. if err != nil {
  359. log.Error("Unable to start transaction: %v", err)
  360. return
  361. }
  362. var followerID int64
  363. if remoteUser != nil {
  364. followerID = remoteUser.ID
  365. } else {
  366. // Add follower locally, since it wasn't found before
  367. res, err := t.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox) VALUES (?, ?, ?)", fullActor.ID, fullActor.Inbox, fullActor.Endpoints.SharedInbox)
  368. if err != nil {
  369. if !app.db.isDuplicateKeyErr(err) {
  370. t.Rollback()
  371. log.Error("Couldn't add new remoteuser in DB: %v\n", err)
  372. return
  373. }
  374. }
  375. followerID, err = res.LastInsertId()
  376. if err != nil {
  377. t.Rollback()
  378. log.Error("no lastinsertid for followers, rolling back: %v", err)
  379. return
  380. }
  381. // Add in key
  382. _, err = t.Exec("INSERT INTO remoteuserkeys (id, remote_user_id, public_key) VALUES (?, ?, ?)", fullActor.PublicKey.ID, followerID, fullActor.PublicKey.PublicKeyPEM)
  383. if err != nil {
  384. if !app.db.isDuplicateKeyErr(err) {
  385. t.Rollback()
  386. log.Error("Couldn't add follower keys in DB: %v\n", err)
  387. return
  388. }
  389. }
  390. }
  391. // Add follow
  392. _, err = t.Exec("INSERT INTO remotefollows (collection_id, remote_user_id, created) VALUES (?, ?, "+app.db.now()+")", c.ID, followerID)
  393. if err != nil {
  394. if !app.db.isDuplicateKeyErr(err) {
  395. t.Rollback()
  396. log.Error("Couldn't add follower in DB: %v\n", err)
  397. return
  398. }
  399. }
  400. err = t.Commit()
  401. if err != nil {
  402. t.Rollback()
  403. log.Error("Rolling back after Commit(): %v\n", err)
  404. return
  405. }
  406. } else if isUnfollow {
  407. // Remove follower locally
  408. _, err = app.db.Exec("DELETE FROM remotefollows WHERE collection_id = ? AND remote_user_id = (SELECT id FROM remoteusers WHERE actor_id = ?)", c.ID, to.String())
  409. if err != nil {
  410. log.Error("Couldn't remove follower from DB: %v\n", err)
  411. }
  412. }
  413. }()
  414. return nil
  415. }
  416. func makeActivityPost(hostName string, p *activitystreams.Person, url string, m interface{}) error {
  417. log.Info("POST %s", url)
  418. b, err := json.Marshal(m)
  419. if err != nil {
  420. return err
  421. }
  422. r, _ := http.NewRequest("POST", url, bytes.NewBuffer(b))
  423. r.Header.Add("Content-Type", "application/activity+json")
  424. r.Header.Set("User-Agent", "Go ("+serverSoftware+"/"+softwareVer+"; +"+hostName+")")
  425. h := sha256.New()
  426. h.Write(b)
  427. r.Header.Add("Digest", "SHA-256="+base64.StdEncoding.EncodeToString(h.Sum(nil)))
  428. // Sign using the 'Signature' header
  429. privKey, err := activitypub.DecodePrivateKey(p.GetPrivKey())
  430. if err != nil {
  431. return err
  432. }
  433. signer := httpsig.NewSigner(p.PublicKey.ID, privKey, httpsig.RSASHA256, []string{"(request-target)", "date", "host", "digest"})
  434. err = signer.SignSigHeader(r)
  435. if err != nil {
  436. log.Error("Can't sign: %v", err)
  437. }
  438. if debugging {
  439. dump, err := httputil.DumpRequestOut(r, true)
  440. if err != nil {
  441. log.Error("Can't dump: %v", err)
  442. } else {
  443. log.Info("%s", dump)
  444. }
  445. }
  446. resp, err := http.DefaultClient.Do(r)
  447. if err != nil {
  448. return err
  449. }
  450. if resp != nil && resp.Body != nil {
  451. defer resp.Body.Close()
  452. }
  453. body, err := ioutil.ReadAll(resp.Body)
  454. if err != nil {
  455. return err
  456. }
  457. if debugging {
  458. log.Info("Status : %s", resp.Status)
  459. log.Info("Response: %s", body)
  460. }
  461. return nil
  462. }
  463. func resolveIRI(hostName, url string) ([]byte, error) {
  464. log.Info("GET %s", url)
  465. r, _ := http.NewRequest("GET", url, nil)
  466. r.Header.Add("Accept", "application/activity+json")
  467. r.Header.Set("User-Agent", "Go ("+serverSoftware+"/"+softwareVer+"; +"+hostName+")")
  468. if debugging {
  469. dump, err := httputil.DumpRequestOut(r, true)
  470. if err != nil {
  471. log.Error("Can't dump: %v", err)
  472. } else {
  473. log.Info("%s", dump)
  474. }
  475. }
  476. resp, err := http.DefaultClient.Do(r)
  477. if err != nil {
  478. return nil, err
  479. }
  480. if resp != nil && resp.Body != nil {
  481. defer resp.Body.Close()
  482. }
  483. body, err := ioutil.ReadAll(resp.Body)
  484. if err != nil {
  485. return nil, err
  486. }
  487. if debugging {
  488. log.Info("Status : %s", resp.Status)
  489. log.Info("Response: %s", body)
  490. }
  491. return body, nil
  492. }
  493. func deleteFederatedPost(app *App, p *PublicPost, collID int64) error {
  494. if debugging {
  495. log.Info("Deleting federated post!")
  496. }
  497. p.Collection.hostName = app.cfg.App.Host
  498. actor := p.Collection.PersonObject(collID)
  499. na := p.ActivityObject(app.cfg)
  500. // Add followers
  501. p.Collection.ID = collID
  502. followers, err := app.db.GetAPFollowers(&p.Collection.Collection)
  503. if err != nil {
  504. log.Error("Couldn't delete post (get followers)! %v", err)
  505. return err
  506. }
  507. inboxes := map[string][]string{}
  508. for _, f := range *followers {
  509. inbox := f.SharedInbox
  510. if inbox == "" {
  511. inbox = f.Inbox
  512. }
  513. if _, ok := inboxes[inbox]; ok {
  514. inboxes[inbox] = append(inboxes[inbox], f.ActorID)
  515. } else {
  516. inboxes[inbox] = []string{f.ActorID}
  517. }
  518. }
  519. for si, instFolls := range inboxes {
  520. na.CC = []string{}
  521. for _, f := range instFolls {
  522. na.CC = append(na.CC, f)
  523. }
  524. err = makeActivityPost(app.cfg.App.Host, actor, si, activitystreams.NewDeleteActivity(na))
  525. if err != nil {
  526. log.Error("Couldn't delete post! %v", err)
  527. }
  528. }
  529. return nil
  530. }
  531. func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error {
  532. if debugging {
  533. if isUpdate {
  534. log.Info("Federating updated post!")
  535. } else {
  536. log.Info("Federating new post!")
  537. }
  538. }
  539. actor := p.Collection.PersonObject(collID)
  540. na := p.ActivityObject(app.cfg)
  541. // Add followers
  542. p.Collection.ID = collID
  543. followers, err := app.db.GetAPFollowers(&p.Collection.Collection)
  544. if err != nil {
  545. log.Error("Couldn't post! %v", err)
  546. return err
  547. }
  548. log.Info("Followers for %d: %+v", collID, followers)
  549. inboxes := map[string][]string{}
  550. for _, f := range *followers {
  551. inbox := f.SharedInbox
  552. if inbox == "" {
  553. inbox = f.Inbox
  554. }
  555. if _, ok := inboxes[inbox]; ok {
  556. inboxes[inbox] = append(inboxes[inbox], f.ActorID)
  557. } else {
  558. inboxes[inbox] = []string{f.ActorID}
  559. }
  560. }
  561. for si, instFolls := range inboxes {
  562. na.CC = []string{}
  563. for _, f := range instFolls {
  564. na.CC = append(na.CC, f)
  565. }
  566. var activity *activitystreams.Activity
  567. if isUpdate {
  568. activity = activitystreams.NewUpdateActivity(na)
  569. } else {
  570. activity = activitystreams.NewCreateActivity(na)
  571. activity.To = na.To
  572. activity.CC = na.CC
  573. }
  574. err = makeActivityPost(app.cfg.App.Host, actor, si, activity)
  575. if err != nil {
  576. log.Error("Couldn't post! %v", err)
  577. }
  578. }
  579. return nil
  580. }
  581. func getRemoteUser(app *App, actorID string) (*RemoteUser, error) {
  582. u := RemoteUser{ActorID: actorID}
  583. err := app.db.QueryRow("SELECT id, inbox, shared_inbox FROM remoteusers WHERE actor_id = ?", actorID).Scan(&u.ID, &u.Inbox, &u.SharedInbox)
  584. switch {
  585. case err == sql.ErrNoRows:
  586. return nil, impart.HTTPError{http.StatusNotFound, "No remote user with that ID."}
  587. case err != nil:
  588. log.Error("Couldn't get remote user %s: %v", actorID, err)
  589. return nil, err
  590. }
  591. return &u, nil
  592. }
  593. func getActor(app *App, actorIRI string) (*activitystreams.Person, *RemoteUser, error) {
  594. log.Info("Fetching actor %s locally", actorIRI)
  595. actor := &activitystreams.Person{}
  596. remoteUser, err := getRemoteUser(app, actorIRI)
  597. if err != nil {
  598. if iErr, ok := err.(impart.HTTPError); ok {
  599. if iErr.Status == http.StatusNotFound {
  600. // Fetch remote actor
  601. log.Info("Not found; fetching actor %s remotely", actorIRI)
  602. actorResp, err := resolveIRI(app.cfg.App.Host, actorIRI)
  603. if err != nil {
  604. log.Error("Unable to get actor! %v", err)
  605. return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't fetch actor."}
  606. }
  607. if err := unmarshalActor(actorResp, actor); err != nil {
  608. log.Error("Unable to unmarshal actor! %v", err)
  609. return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't parse actor."}
  610. }
  611. } else {
  612. return nil, nil, err
  613. }
  614. } else {
  615. return nil, nil, err
  616. }
  617. } else {
  618. actor = remoteUser.AsPerson()
  619. }
  620. return actor, remoteUser, nil
  621. }
  622. // unmarshal actor normalizes the actor response to conform to
  623. // the type Person from github.com/writeas/web-core/activitysteams
  624. //
  625. // some implementations return different context field types
  626. // this converts any non-slice contexts into a slice
  627. func unmarshalActor(actorResp []byte, actor *activitystreams.Person) error {
  628. // FIXME: Hubzilla has an object for the Actor's url: cannot unmarshal object into Go struct field Person.url of type string
  629. // flexActor overrides the Context field to allow
  630. // all valid representations during unmarshal
  631. flexActor := struct {
  632. activitystreams.Person
  633. Context json.RawMessage `json:"@context,omitempty"`
  634. }{}
  635. if err := json.Unmarshal(actorResp, &flexActor); err != nil {
  636. return err
  637. }
  638. actor.Endpoints = flexActor.Endpoints
  639. actor.Followers = flexActor.Followers
  640. actor.Following = flexActor.Following
  641. actor.ID = flexActor.ID
  642. actor.Icon = flexActor.Icon
  643. actor.Inbox = flexActor.Inbox
  644. actor.Name = flexActor.Name
  645. actor.Outbox = flexActor.Outbox
  646. actor.PreferredUsername = flexActor.PreferredUsername
  647. actor.PublicKey = flexActor.PublicKey
  648. actor.Summary = flexActor.Summary
  649. actor.Type = flexActor.Type
  650. actor.URL = flexActor.URL
  651. func(val interface{}) {
  652. switch val.(type) {
  653. case []interface{}:
  654. // already a slice, do nothing
  655. actor.Context = val.([]interface{})
  656. default:
  657. actor.Context = []interface{}{val}
  658. }
  659. }(flexActor.Context)
  660. return nil
  661. }