diff --git a/internal/api/client/account/block.go b/internal/api/client/account/block.go
index c8cf099f9..c83837c2a 100644
--- a/internal/api/client/account/block.go
+++ b/internal/api/client/account/block.go
@@ -25,6 +25,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
+// AccountBlockPOSTHandler handles the creation of a block from the authed account targeting the given account ID.
func (m *Module) AccountBlockPOSTHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
diff --git a/internal/api/client/account/unblock.go b/internal/api/client/account/unblock.go
index ec237dd06..1cb959db9 100644
--- a/internal/api/client/account/unblock.go
+++ b/internal/api/client/account/unblock.go
@@ -25,6 +25,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
+// AccountUnblockPOSTHandler handles the removal of a block from the authed account targeting the given account ID.
func (m *Module) AccountUnblockPOSTHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
diff --git a/internal/federation/dereference.go b/internal/federation/dereference.go
index 20ffa3a8d..b87462acd 100644
--- a/internal/federation/dereference.go
+++ b/internal/federation/dereference.go
@@ -393,6 +393,7 @@ func (f *federator) DereferenceAnnounce(announce *gtsmodel.Status, requestingUse
announce.Language = boostedStatus.Language
announce.Text = boostedStatus.Text
announce.BoostOfID = boostedStatus.ID
+ announce.BoostOfAccountID = boostedStatus.AccountID
announce.Visibility = boostedStatus.Visibility
announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced
announce.GTSBoostedStatus = boostedStatus
@@ -477,6 +478,7 @@ func (f *federator) DereferenceAnnounce(announce *gtsmodel.Status, requestingUse
announce.Language = boostedStatus.Language
announce.Text = boostedStatus.Text
announce.BoostOfID = boostedStatus.ID
+ announce.BoostOfAccountID = boostedStatus.AccountID
announce.Visibility = boostedStatus.Visibility
announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced
announce.GTSBoostedStatus = boostedStatus
diff --git a/internal/gtsmodel/status.go b/internal/gtsmodel/status.go
index caa5a2a25..84b3dfc7c 100644
--- a/internal/gtsmodel/status.go
+++ b/internal/gtsmodel/status.go
@@ -56,6 +56,8 @@ type Status struct {
InReplyToAccountID string `pg:"type:CHAR(26)"`
// id of the status this status is a boost of
BoostOfID string `pg:"type:CHAR(26)"`
+ // id of the account that owns the boosted status
+ BoostOfAccountID string `pg:"type:CHAR(26)"`
// cw string for this status
ContentWarning string
// visibility entry for this status
diff --git a/internal/processing/account/get.go b/internal/processing/account/get.go
index aba1ed14a..b937ace5b 100644
--- a/internal/processing/account/get.go
+++ b/internal/processing/account/get.go
@@ -45,9 +45,19 @@ func (p *processor) Get(requestingAccount *gtsmodel.Account, targetAccountID str
p.log.WithField("func", "AccountGet").Debugf("dereferencing account: %s", err)
}
- var mastoAccount *apimodel.Account
+ var blocked bool
var err error
- if requestingAccount != nil && targetAccount.ID == requestingAccount.ID {
+ if requestingAccount != nil {
+ blocked, err = p.db.Blocked(requestingAccount.ID, targetAccountID)
+ if err != nil {
+ return nil, fmt.Errorf("error checking account block: %s", err)
+ }
+ }
+
+ var mastoAccount *apimodel.Account
+ if blocked {
+ mastoAccount, err = p.tc.AccountToMastoBlocked(targetAccount)
+ } else if requestingAccount != nil && targetAccount.ID == requestingAccount.ID {
mastoAccount, err = p.tc.AccountToMastoSensitive(targetAccount)
} else {
mastoAccount, err = p.tc.AccountToMastoPublic(targetAccount)
diff --git a/internal/processing/blocks.go b/internal/processing/blocks.go
index 31f57fc7c..509600ca6 100644
--- a/internal/processing/blocks.go
+++ b/internal/processing/blocks.go
@@ -43,7 +43,7 @@ func (p *processor) BlocksGet(authed *oauth.Auth, maxID string, sinceID string,
apiAccounts := []*apimodel.Account{}
for _, a := range accounts {
- apiAccount, err := p.tc.AccountToMastoPublic(a)
+ apiAccount, err := p.tc.AccountToMastoBlocked(a)
if err != nil {
continue
}
diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go
index e7f757f10..6755a9d82 100644
--- a/internal/processing/fromclientapi.go
+++ b/internal/processing/fromclientapi.go
@@ -99,7 +99,14 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error
return errors.New("block was not parseable as *gtsmodel.Block")
}
- // TODO: remove any of the blocking account's statuses from the blocked account's timeline
+ // remove any of the blocking account's statuses from the blocked account's timeline, and vice versa
+ if err := p.timelineManager.WipeStatusesFromAccountID(block.AccountID, block.TargetAccountID); err != nil {
+ return err
+ }
+ if err := p.timelineManager.WipeStatusesFromAccountID(block.TargetAccountID, block.AccountID); err != nil {
+ return err
+ }
+
// TODO: same with notifications
// TODO: same with bookmarks
diff --git a/internal/processing/fromfederator.go b/internal/processing/fromfederator.go
index f324af15f..94a4e5af8 100644
--- a/internal/processing/fromfederator.go
+++ b/internal/processing/fromfederator.go
@@ -129,8 +129,18 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
}
case gtsmodel.ActivityStreamsBlock:
// CREATE A BLOCK
+ block, ok := federatorMsg.GTSModel.(*gtsmodel.Block)
+ if !ok {
+ return errors.New("block was not parseable as *gtsmodel.Block")
+ }
- // TODO: remove any of the blocking account's statuses from the blocked account's timeline
+ // remove any of the blocking account's statuses from the blocked account's timeline, and vice versa
+ if err := p.timelineManager.WipeStatusesFromAccountID(block.AccountID, block.TargetAccountID); err != nil {
+ return err
+ }
+ if err := p.timelineManager.WipeStatusesFromAccountID(block.TargetAccountID, block.AccountID); err != nil {
+ return err
+ }
// TODO: same with notifications
// TODO: same with bookmarks
}
diff --git a/internal/timeline/get.go b/internal/timeline/get.go
index f07d81d55..d7ebb7766 100644
--- a/internal/timeline/get.go
+++ b/internal/timeline/get.go
@@ -1,3 +1,21 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
package timeline
import (
diff --git a/internal/timeline/index.go b/internal/timeline/index.go
index 8c6b0d578..8dd7fee97 100644
--- a/internal/timeline/index.go
+++ b/internal/timeline/index.go
@@ -1,3 +1,21 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
package timeline
import (
@@ -44,7 +62,7 @@ grabloop:
}
for _, s := range filtered {
- if _, err := t.IndexOne(s.CreatedAt, s.ID, s.BoostOfID); err != nil {
+ if _, err := t.IndexOne(s.CreatedAt, s.ID, s.BoostOfID, s.AccountID, s.BoostOfAccountID); err != nil {
return fmt.Errorf("IndexBefore: error indexing status with id %s: %s", s.ID, err)
}
}
@@ -79,7 +97,7 @@ grabloop:
}
for _, s := range filtered {
- if _, err := t.IndexOne(s.CreatedAt, s.ID, s.BoostOfID); err != nil {
+ if _, err := t.IndexOne(s.CreatedAt, s.ID, s.BoostOfID, s.AccountID, s.BoostOfAccountID); err != nil {
return fmt.Errorf("IndexBehind: error indexing status with id %s: %s", s.ID, err)
}
}
@@ -91,24 +109,29 @@ func (t *timeline) IndexOneByID(statusID string) error {
return nil
}
-func (t *timeline) IndexOne(statusCreatedAt time.Time, statusID string, boostOfID string) (bool, error) {
+func (t *timeline) IndexOne(statusCreatedAt time.Time, statusID string, boostOfID string, accountID string, boostOfAccountID string) (bool, error) {
t.Lock()
defer t.Unlock()
postIndexEntry := &postIndexEntry{
- statusID: statusID,
- boostOfID: boostOfID,
+ statusID: statusID,
+ boostOfID: boostOfID,
+ accountID: accountID,
+ boostOfAccountID: boostOfAccountID,
}
return t.postIndex.insertIndexed(postIndexEntry)
}
-func (t *timeline) IndexAndPrepareOne(statusCreatedAt time.Time, statusID string) (bool, error) {
+func (t *timeline) IndexAndPrepareOne(statusCreatedAt time.Time, statusID string, boostOfID string, accountID string, boostOfAccountID string) (bool, error) {
t.Lock()
defer t.Unlock()
postIndexEntry := &postIndexEntry{
- statusID: statusID,
+ statusID: statusID,
+ boostOfID: boostOfID,
+ accountID: accountID,
+ boostOfAccountID: boostOfAccountID,
}
inserted, err := t.postIndex.insertIndexed(postIndexEntry)
diff --git a/internal/timeline/manager.go b/internal/timeline/manager.go
index d50c9f783..00d87bb26 100644
--- a/internal/timeline/manager.go
+++ b/internal/timeline/manager.go
@@ -78,6 +78,8 @@ type Manager interface {
Remove(statusID string, timelineAccountID string) (int, error)
// WipeStatusFromAllTimelines removes one status from the index and prepared posts of all timelines
WipeStatusFromAllTimelines(statusID string) error
+ // WipeStatusesFromAccountID removes all statuses by the given accountID from the timelineAccountID's timelines.
+ WipeStatusesFromAccountID(accountID string, timelineAccountID string) error
}
// NewManager returns a new timeline manager with the given database, typeconverter, config, and log.
@@ -112,7 +114,7 @@ func (m *manager) Ingest(status *gtsmodel.Status, timelineAccountID string) (boo
}
l.Trace("ingesting status")
- return t.IndexOne(status.CreatedAt, status.ID, status.BoostOfID)
+ return t.IndexOne(status.CreatedAt, status.ID, status.BoostOfID, status.AccountID, status.BoostOfAccountID)
}
func (m *manager) IngestAndPrepare(status *gtsmodel.Status, timelineAccountID string) (bool, error) {
@@ -128,7 +130,7 @@ func (m *manager) IngestAndPrepare(status *gtsmodel.Status, timelineAccountID st
}
l.Trace("ingesting status")
- return t.IndexAndPrepareOne(status.CreatedAt, status.ID)
+ return t.IndexAndPrepareOne(status.CreatedAt, status.ID, status.BoostOfID, status.AccountID, status.BoostOfAccountID)
}
func (m *manager) Remove(statusID string, timelineAccountID string) (int, error) {
@@ -219,6 +221,16 @@ func (m *manager) WipeStatusFromAllTimelines(statusID string) error {
return err
}
+func (m *manager) WipeStatusesFromAccountID(accountID string, timelineAccountID string) error {
+ t, err := m.getOrCreateTimeline(timelineAccountID)
+ if err != nil {
+ return err
+ }
+
+ _, err = t.RemoveAllBy(accountID)
+ return err
+}
+
func (m *manager) getOrCreateTimeline(timelineAccountID string) (Timeline, error) {
var t Timeline
i, ok := m.accountTimelines.Load(timelineAccountID)
diff --git a/internal/timeline/postindex.go b/internal/timeline/postindex.go
index 44765bf50..db155d0fe 100644
--- a/internal/timeline/postindex.go
+++ b/internal/timeline/postindex.go
@@ -1,3 +1,21 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
package timeline
import (
@@ -10,8 +28,10 @@ type postIndex struct {
}
type postIndexEntry struct {
- statusID string
- boostOfID string
+ statusID string
+ boostOfID string
+ accountID string
+ boostOfAccountID string
}
func (p *postIndex) insertIndexed(i *postIndexEntry) (bool, error) {
diff --git a/internal/timeline/prepare.go b/internal/timeline/prepare.go
index ac85d92e9..0fbd8ebba 100644
--- a/internal/timeline/prepare.go
+++ b/internal/timeline/prepare.go
@@ -1,3 +1,21 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
package timeline
import (
@@ -207,8 +225,11 @@ func (t *timeline) prepare(statusID string) error {
// shove it in prepared posts as a prepared posts entry
preparedPostsEntry := &preparedPostsEntry{
- statusID: statusID,
- prepared: apiModelStatus,
+ statusID: gtsStatus.ID,
+ boostOfID: gtsStatus.BoostOfID,
+ accountID: gtsStatus.AccountID,
+ boostOfAccountID: gtsStatus.BoostOfAccountID,
+ prepared: apiModelStatus,
}
return t.preparedPosts.insertPrepared(preparedPostsEntry)
diff --git a/internal/timeline/preparedposts.go b/internal/timeline/preparedposts.go
index 1976189c8..7f8d84357 100644
--- a/internal/timeline/preparedposts.go
+++ b/internal/timeline/preparedposts.go
@@ -1,3 +1,21 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
package timeline
import (
@@ -12,8 +30,11 @@ type preparedPosts struct {
}
type preparedPostsEntry struct {
- statusID string
- prepared *apimodel.Status
+ statusID string
+ boostOfID string
+ accountID string
+ boostOfAccountID string
+ prepared *apimodel.Status
}
func (p *preparedPosts) insertPrepared(i *preparedPostsEntry) error {
diff --git a/internal/timeline/remove.go b/internal/timeline/remove.go
index 8842c60cb..ee4f112c5 100644
--- a/internal/timeline/remove.go
+++ b/internal/timeline/remove.go
@@ -1,3 +1,21 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
package timeline
import (
@@ -58,3 +76,55 @@ func (t *timeline) Remove(statusID string) (int, error) {
l.Debugf("removed %d entries", removed)
return removed, nil
}
+
+func (t *timeline) RemoveAllBy(accountID string) (int, error) {
+ l := t.log.WithFields(logrus.Fields{
+ "func": "RemoveAllBy",
+ "accountTimeline": t.accountID,
+ "accountID": accountID,
+ })
+ t.Lock()
+ defer t.Unlock()
+ var removed int
+
+ // remove entr(ies) from the post index
+ removeIndexes := []*list.Element{}
+ if t.postIndex != nil && t.postIndex.data != nil {
+ for e := t.postIndex.data.Front(); e != nil; e = e.Next() {
+ entry, ok := e.Value.(*postIndexEntry)
+ if !ok {
+ return removed, errors.New("Remove: could not parse e as a postIndexEntry")
+ }
+ if entry.accountID == accountID || entry.boostOfAccountID == accountID {
+ l.Debug("found status in postIndex")
+ removeIndexes = append(removeIndexes, e)
+ }
+ }
+ }
+ for _, e := range removeIndexes {
+ t.postIndex.data.Remove(e)
+ removed = removed + 1
+ }
+
+ // remove entr(ies) from prepared posts
+ removePrepared := []*list.Element{}
+ if t.preparedPosts != nil && t.preparedPosts.data != nil {
+ for e := t.preparedPosts.data.Front(); e != nil; e = e.Next() {
+ entry, ok := e.Value.(*preparedPostsEntry)
+ if !ok {
+ return removed, errors.New("Remove: could not parse e as a preparedPostsEntry")
+ }
+ if entry.accountID == accountID || entry.boostOfAccountID == accountID {
+ l.Debug("found status in preparedPosts")
+ removePrepared = append(removePrepared, e)
+ }
+ }
+ }
+ for _, e := range removePrepared {
+ t.preparedPosts.data.Remove(e)
+ removed = removed + 1
+ }
+
+ l.Debugf("removed %d entries", removed)
+ return removed, nil
+}
diff --git a/internal/timeline/timeline.go b/internal/timeline/timeline.go
index d0fadb19e..fe811a303 100644
--- a/internal/timeline/timeline.go
+++ b/internal/timeline/timeline.go
@@ -65,7 +65,7 @@ type Timeline interface {
//
// The returned bool indicates whether or not the status was actually inserted into the timeline. This will be false
// if the status is a boost and the original post or another boost of it already exists < boostReinsertionDepth back in the timeline.
- IndexOne(statusCreatedAt time.Time, statusID string, boostOfID string) (bool, error)
+ IndexOne(statusCreatedAt time.Time, statusID string, boostOfID string, accountID string, boostOfAccountID string) (bool, error)
// OldestIndexedPostID returns the id of the rearmost (ie., the oldest) indexed post, or an error if something goes wrong.
// If nothing goes wrong but there's no oldest post, an empty string will be returned so make sure to check for this.
@@ -85,7 +85,7 @@ type Timeline interface {
//
// The returned bool indicates whether or not the status was actually inserted into the timeline. This will be false
// if the status is a boost and the original post or another boost of it already exists < boostReinsertionDepth back in the timeline.
- IndexAndPrepareOne(statusCreatedAt time.Time, statusID string) (bool, error)
+ IndexAndPrepareOne(statusCreatedAt time.Time, statusID string, boostOfID string, accountID string, boostOfAccountID string) (bool, error)
// OldestPreparedPostID returns the id of the rearmost (ie., the oldest) prepared post, or an error if something goes wrong.
// If nothing goes wrong but there's no oldest post, an empty string will be returned so make sure to check for this.
OldestPreparedPostID() (string, error)
@@ -109,6 +109,10 @@ type Timeline interface {
//
// The returned int indicates the amount of entries that were removed.
Remove(statusID string) (int, error)
+ // RemoveAllBy removes all statuses by the given accountID, from both the index and prepared posts.
+ //
+ // The returned int indicates the amount of entries that were removed.
+ RemoveAllBy(accountID string) (int, error)
}
// timeline fulfils the Timeline interface
diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go
index 203b4019a..57c2a1f6d 100644
--- a/internal/typeutils/converter.go
+++ b/internal/typeutils/converter.go
@@ -48,6 +48,10 @@ type TypeConverter interface {
// if something goes wrong. The returned account should be ready to serialize on an API level, and may NOT have sensitive fields.
// In other words, this is the public record that the server has of an account.
AccountToMastoPublic(account *gtsmodel.Account) (*model.Account, error)
+ // AccountToMastoBlocked takes a db model account as a param, and returns a mastotype account, or an error if
+ // something goes wrong. The returned account will be a bare minimum representation of the account. This function should be used
+ // when someone wants to view an account they've blocked.
+ AccountToMastoBlocked(account *gtsmodel.Account) (*model.Account, error)
// AppToMastoSensitive takes a db model application as a param, and returns a populated mastotype application, or an error
// if something goes wrong. The returned application should be ready to serialize on an API level, and may have sensitive fields
// (such as client id and client secret), so serve it only to an authorized user who should have permission to see it.
diff --git a/internal/typeutils/internal.go b/internal/typeutils/internal.go
index b081708a2..a46ad7fbd 100644
--- a/internal/typeutils/internal.go
+++ b/internal/typeutils/internal.go
@@ -67,6 +67,7 @@ func (c *converter) StatusToBoost(s *gtsmodel.Status, boostingAccount *gtsmodel.
Language: s.Language,
Text: s.Text,
BoostOfID: s.ID,
+ BoostOfAccountID: s.AccountID,
Visibility: s.Visibility,
VisibilityAdvanced: s.VisibilityAdvanced,
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
index 61c11b8ef..088e30d68 100644
--- a/internal/typeutils/internaltofrontend.go
+++ b/internal/typeutils/internaltofrontend.go
@@ -173,6 +173,27 @@ func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*model.Account, e
}, nil
}
+func (c *converter) AccountToMastoBlocked(a *gtsmodel.Account) (*model.Account, error) {
+ var acct string
+ if a.Domain != "" {
+ // this is a remote user
+ acct = fmt.Sprintf("%s@%s", a.Username, a.Domain)
+ } else {
+ // this is a local user
+ acct = a.Username
+ }
+
+ return &model.Account{
+ ID: a.ID,
+ Username: a.Username,
+ Acct: acct,
+ DisplayName: a.Username,
+ Bot: a.Bot,
+ CreatedAt: a.CreatedAt.Format(time.RFC3339),
+ URL: a.URL,
+ }, nil
+}
+
func (c *converter) AppToMastoSensitive(a *gtsmodel.Application) (*model.Application, error) {
return &model.Application{
ID: a.ID,