diff --git a/sam/docs/README.md b/sam/docs/README.md
index 6762c1cac..8dab3d3cb 100644
--- a/sam/docs/README.md
+++ b/sam/docs/README.md
@@ -280,7 +280,7 @@ A channel is a representation of a sequence of messages. It has meta data like c
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| channelID | uint64 | PATH | Channel ID | N/A | YES |
-| state | string | POST | Valid values: delete, restore, archive, unarchive | N/A | YES |
+| state | string | POST | Valid values: delete, undelete, archive, unarchive | N/A | YES |
## Read channel details
@@ -453,21 +453,6 @@ The following event types may be sent with a message event:
| --------- | ---- | ------ | ----------- | ------- | --------- |
| messageID | uint64 | PATH | Message ID | N/A | YES |
-## Search messages
-
-#### Method
-
-| URI | Protocol | Method | Authentication |
-| --- | -------- | ------ | -------------- |
-| `/channels/{channelID}/messages/search` | HTTP/S | GET | Client ID, Session ID |
-
-#### Request parameters
-
-| Parameter | Type | Method | Description | Default | Required? |
-| --------- | ---- | ------ | ----------- | ------- | --------- |
-| query | string | GET | Search string to match against messages | N/A | NO |
-| message_type | string | GET | Limit results to message type | N/A
Values:
- `history`
- `message`
- `attachment`
- `media`
| NO |
-
## Pin message to channel (public bookmark)
#### Method
@@ -618,6 +603,28 @@ The following event types may be sent with a message event:
+# Search entry point
+
+## Search for messages
+
+#### Method
+
+| URI | Protocol | Method | Authentication |
+| --- | -------- | ------ | -------------- |
+| `/search/messages` | HTTP/S | GET | Client ID, Session ID |
+
+#### Request parameters
+
+| Parameter | Type | Method | Description | Default | Required? |
+| --------- | ---- | ------ | ----------- | ------- | --------- |
+| inChannel | uint64 | GET | Search only in one channel | N/A | NO |
+| fromUser | uint64 | GET | Search only from one user | N/A | NO |
+| firstID | uint64 | GET | Paging; return newer messages only (higher id) | N/A | NO |
+| lastID | uint64 | GET | Paging; return older messages only (lower id) | N/A | NO |
+
+
+
+
# Users
## Search users (Directory)
diff --git a/sam/docs/src/spec.json b/sam/docs/src/spec.json
index 9ed77213f..476143c9a 100644
--- a/sam/docs/src/spec.json
+++ b/sam/docs/src/spec.json
@@ -387,24 +387,6 @@
]
}
},
- {
- "name": "search",
- "path": "/search",
- "method": "GET",
- "title": "Search messages",
- "parameters": {
- "get": [
- { "type": "string", "name": "query", "required": false, "title": "Search string to match against messages" },
- {
- "type": "string",
- "name": "message_type",
- "required": false,
- "title": "Limit results to message type",
- "values": ["history", "message", "attachment", "media"]
- }
- ]
- }
- },
{
"name": "pin",
"path": "/{messageID}/pin",
@@ -532,6 +514,37 @@
}
]
},
+ {
+ "name": "search",
+ "entrypoint": "search",
+ "authentication": ["Client ID", "Session ID"],
+ "path": "/search",
+ "method": "GET",
+ "title": "Search entry point",
+ "parameters": {
+ "get": [
+ { "name": "query", "type": "string", "required": false, "title": "Search query" }
+ ]
+ },
+
+ "apis": [
+ {
+ "method": "GET",
+ "name": "messages",
+ "path": "/messages",
+ "title": "Search for messages",
+
+ "parameters": {
+ "GET": [
+ { "name": "inChannel", "type": "uint64", "required": false, "title": "Search only in one channel"},
+ { "name": "fromUser", "type": "uint64", "required": false, "title": "Search only from one user"},
+ { "name": "firstID", "type": "uint64", "required": false, "title": "Paging; return newer messages only (higher id)"},
+ { "name": "lastID", "type": "uint64", "required": false, "title": "Paging; return older messages only (lower id)"}
+ ]
+ }
+ }
+ ]
+ },
{
"title": "Users",
"package": "sam",
diff --git a/sam/docs/src/spec/channel.json b/sam/docs/src/spec/channel.json
index 59ebf3c03..b8383dc94 100644
--- a/sam/docs/src/spec/channel.json
+++ b/sam/docs/src/spec/channel.json
@@ -122,7 +122,7 @@
{
"name": "state",
"required": true,
- "title": "Valid values: delete, restore, archive, unarchive",
+ "title": "Valid values: delete, undelete, archive, unarchive",
"type": "string"
}
]
diff --git a/sam/docs/src/spec/message.json b/sam/docs/src/spec/message.json
index 3a8c10e40..d8cb4bc43 100644
--- a/sam/docs/src/spec/message.json
+++ b/sam/docs/src/spec/message.json
@@ -92,34 +92,6 @@
]
}
},
- {
- "Name": "search",
- "Method": "GET",
- "Title": "Search messages",
- "Path": "/search",
- "Parameters": {
- "get": [
- {
- "name": "query",
- "required": false,
- "title": "Search string to match against messages",
- "type": "string"
- },
- {
- "name": "message_type",
- "required": false,
- "title": "Limit results to message type",
- "type": "string",
- "values": [
- "history",
- "message",
- "attachment",
- "media"
- ]
- }
- ]
- }
- },
{
"Name": "pin",
"Method": "POST",
diff --git a/sam/docs/src/spec/search.json b/sam/docs/src/spec/search.json
new file mode 100644
index 000000000..3fc6b69a9
--- /dev/null
+++ b/sam/docs/src/spec/search.json
@@ -0,0 +1,58 @@
+{
+ "Title": "Search entry point",
+ "Package": "",
+ "Interface": "Search",
+ "Struct": null,
+ "Parameters": {
+ "get": [
+ {
+ "name": "query",
+ "required": false,
+ "title": "Search query",
+ "type": "string"
+ }
+ ]
+ },
+ "Protocol": "",
+ "Authentication": [
+ "Client ID",
+ "Session ID"
+ ],
+ "Path": "/search",
+ "APIs": [
+ {
+ "Name": "messages",
+ "Method": "GET",
+ "Title": "Search for messages",
+ "Path": "/messages",
+ "Parameters": {
+ "GET": [
+ {
+ "name": "inChannel",
+ "required": false,
+ "title": "Search only in one channel",
+ "type": "uint64"
+ },
+ {
+ "name": "fromUser",
+ "required": false,
+ "title": "Search only from one user",
+ "type": "uint64"
+ },
+ {
+ "name": "firstID",
+ "required": false,
+ "title": "Paging; return newer messages only (higher id)",
+ "type": "uint64"
+ },
+ {
+ "name": "lastID",
+ "required": false,
+ "title": "Paging; return older messages only (lower id)",
+ "type": "uint64"
+ }
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/sam/repository/message.go b/sam/repository/message.go
index 055b0706a..7294e3340 100644
--- a/sam/repository/message.go
+++ b/sam/repository/message.go
@@ -95,7 +95,7 @@ func (r *message) FindMessages(filter *types.MessageFilter) (types.MessageSet, e
if filter.Query != "" {
sql += " AND message LIKE ?"
- params = append(params, filter.Query+"%")
+ params = append(params, "%"+filter.Query+"%")
}
if filter.ChannelID > 0 {
@@ -103,6 +103,11 @@ func (r *message) FindMessages(filter *types.MessageFilter) (types.MessageSet, e
params = append(params, filter.ChannelID)
}
+ if filter.UserID > 0 {
+ sql += " AND rel_user = ? "
+ params = append(params, filter.UserID)
+ }
+
if filter.RepliesTo > 0 {
sql += " AND reply_to = ? "
params = append(params, filter.RepliesTo)
diff --git a/sam/rest/handlers/message.go b/sam/rest/handlers/message.go
index bf24c46be..79daa2ad3 100644
--- a/sam/rest/handlers/message.go
+++ b/sam/rest/handlers/message.go
@@ -31,7 +31,6 @@ type MessageAPI interface {
History(context.Context, *request.MessageHistory) (interface{}, error)
Edit(context.Context, *request.MessageEdit) (interface{}, error)
Delete(context.Context, *request.MessageDelete) (interface{}, error)
- Search(context.Context, *request.MessageSearch) (interface{}, error)
Pin(context.Context, *request.MessagePin) (interface{}, error)
GetReplies(context.Context, *request.MessageGetReplies) (interface{}, error)
CreateReply(context.Context, *request.MessageCreateReply) (interface{}, error)
@@ -48,7 +47,6 @@ type Message struct {
History func(http.ResponseWriter, *http.Request)
Edit func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
- Search func(http.ResponseWriter, *http.Request)
Pin func(http.ResponseWriter, *http.Request)
GetReplies func(http.ResponseWriter, *http.Request)
CreateReply func(http.ResponseWriter, *http.Request)
@@ -89,13 +87,6 @@ func NewMessage(mh MessageAPI) *Message {
return mh.Delete(r.Context(), params)
})
},
- Search: func(w http.ResponseWriter, r *http.Request) {
- defer r.Body.Close()
- params := request.NewMessageSearch()
- resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
- return mh.Search(r.Context(), params)
- })
- },
Pin: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewMessagePin()
@@ -163,7 +154,6 @@ func (mh *Message) MountRoutes(r chi.Router, middlewares ...func(http.Handler) h
r.Get("/", mh.History)
r.Put("/{messageID}", mh.Edit)
r.Delete("/{messageID}", mh.Delete)
- r.Get("/search", mh.Search)
r.Post("/{messageID}/pin", mh.Pin)
r.Get("/{messageID}/replies", mh.GetReplies)
r.Post("/{messageID}/replies", mh.CreateReply)
diff --git a/sam/rest/handlers/search.go b/sam/rest/handlers/search.go
new file mode 100644
index 000000000..19347018f
--- /dev/null
+++ b/sam/rest/handlers/search.go
@@ -0,0 +1,57 @@
+package handlers
+
+/*
+ Hello! This file is auto-generated from `docs/src/spec.json`.
+
+ For development:
+ In order to update the generated files, edit this file under the location,
+ add your struct fields, imports, API definitions and whatever you want, and:
+
+ 1. run [spec](https://github.com/titpetric/spec) in the same folder,
+ 2. run `./_gen.php` in this folder.
+
+ You may edit `search.go`, `search.util.go` or `search_test.go` to
+ implement your API calls, helper functions and tests. The file `search.go`
+ is only generated the first time, and will not be overwritten if it exists.
+*/
+
+import (
+ "context"
+ "github.com/go-chi/chi"
+ "net/http"
+
+ "github.com/titpetric/factory/resputil"
+
+ "github.com/crusttech/crust/sam/rest/request"
+)
+
+// Internal API interface
+type SearchAPI interface {
+ Messages(context.Context, *request.SearchMessages) (interface{}, error)
+}
+
+// HTTP API interface
+type Search struct {
+ Messages func(http.ResponseWriter, *http.Request)
+}
+
+func NewSearch(sh SearchAPI) *Search {
+ return &Search{
+ Messages: func(w http.ResponseWriter, r *http.Request) {
+ defer r.Body.Close()
+ params := request.NewSearchMessages()
+ resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
+ return sh.Messages(r.Context(), params)
+ })
+ },
+ }
+}
+
+func (sh *Search) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
+ r.Group(func(r chi.Router) {
+ r.Use(middlewares...)
+ r.Route("/search", func(r chi.Router) {
+ r.Get("/messages", sh.Messages)
+ })
+ })
+}
diff --git a/sam/rest/message.go b/sam/rest/message.go
index e5d157585..3687eb866 100644
--- a/sam/rest/message.go
+++ b/sam/rest/message.go
@@ -68,13 +68,6 @@ func (ctrl *Message) Delete(ctx context.Context, r *request.MessageDelete) (inte
return nil, ctrl.svc.msg.With(ctx).Delete(r.MessageID)
}
-func (ctrl *Message) Search(ctx context.Context, r *request.MessageSearch) (interface{}, error) {
- return ctrl.wrapSet(ctx)(ctrl.svc.msg.With(ctx).Find(&types.MessageFilter{
- ChannelID: r.ChannelID,
- Query: r.Query,
- }))
-}
-
func (ctrl *Message) Pin(ctx context.Context, r *request.MessagePin) (interface{}, error) {
return nil, ctrl.svc.msg.With(ctx).Pin(r.MessageID)
}
diff --git a/sam/rest/request/message.go b/sam/rest/request/message.go
index a6898787a..f713bbe58 100644
--- a/sam/rest/request/message.go
+++ b/sam/rest/request/message.go
@@ -221,59 +221,6 @@ func (m *MessageDelete) Fill(r *http.Request) (err error) {
var _ RequestFiller = NewMessageDelete()
-// Message search request parameters
-type MessageSearch struct {
- Query string
- Message_type string
- ChannelID uint64 `json:",string"`
-}
-
-func NewMessageSearch() *MessageSearch {
- return &MessageSearch{}
-}
-
-func (m *MessageSearch) Fill(r *http.Request) (err error) {
- if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
- err = json.NewDecoder(r.Body).Decode(m)
-
- switch {
- case err == io.EOF:
- err = nil
- case err != nil:
- return errors.Wrap(err, "error parsing http request body")
- }
- }
-
- if err = r.ParseForm(); err != nil {
- return err
- }
-
- get := map[string]string{}
- post := map[string]string{}
- urlQuery := r.URL.Query()
- for name, param := range urlQuery {
- get[name] = string(param[0])
- }
- postVars := r.Form
- for name, param := range postVars {
- post[name] = string(param[0])
- }
-
- if val, ok := get["query"]; ok {
-
- m.Query = val
- }
- if val, ok := get["message_type"]; ok {
-
- m.Message_type = val
- }
- m.ChannelID = parseUInt64(chi.URLParam(r, "channelID"))
-
- return err
-}
-
-var _ RequestFiller = NewMessageSearch()
-
// Message pin request parameters
type MessagePin struct {
MessageID uint64 `json:",string"`
diff --git a/sam/rest/request/search.go b/sam/rest/request/search.go
new file mode 100644
index 000000000..cedd8f159
--- /dev/null
+++ b/sam/rest/request/search.go
@@ -0,0 +1,97 @@
+package request
+
+/*
+ Hello! This file is auto-generated from `docs/src/spec.json`.
+
+ For development:
+ In order to update the generated files, edit this file under the location,
+ add your struct fields, imports, API definitions and whatever you want, and:
+
+ 1. run [spec](https://github.com/titpetric/spec) in the same folder,
+ 2. run `./_gen.php` in this folder.
+
+ You may edit `search.go`, `search.util.go` or `search_test.go` to
+ implement your API calls, helper functions and tests. The file `search.go`
+ is only generated the first time, and will not be overwritten if it exists.
+*/
+
+import (
+ "encoding/json"
+ "github.com/go-chi/chi"
+ "github.com/jmoiron/sqlx/types"
+ "github.com/pkg/errors"
+ "io"
+ "mime/multipart"
+ "net/http"
+ "strings"
+)
+
+var _ = chi.URLParam
+var _ = types.JSONText{}
+var _ = multipart.FileHeader{}
+
+// Search messages request parameters
+type SearchMessages struct {
+ InChannel uint64 `json:",string"`
+ FromUser uint64 `json:",string"`
+ FirstID uint64 `json:",string"`
+ LastID uint64 `json:",string"`
+ Query string
+}
+
+func NewSearchMessages() *SearchMessages {
+ return &SearchMessages{}
+}
+
+func (s *SearchMessages) Fill(r *http.Request) (err error) {
+ if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
+ err = json.NewDecoder(r.Body).Decode(s)
+
+ switch {
+ case err == io.EOF:
+ err = nil
+ case err != nil:
+ return errors.Wrap(err, "error parsing http request body")
+ }
+ }
+
+ if err = r.ParseForm(); err != nil {
+ return err
+ }
+
+ get := map[string]string{}
+ post := map[string]string{}
+ urlQuery := r.URL.Query()
+ for name, param := range urlQuery {
+ get[name] = string(param[0])
+ }
+ postVars := r.Form
+ for name, param := range postVars {
+ post[name] = string(param[0])
+ }
+
+ if val, ok := get["inChannel"]; ok {
+
+ s.InChannel = parseUInt64(val)
+ }
+ if val, ok := get["fromUser"]; ok {
+
+ s.FromUser = parseUInt64(val)
+ }
+ if val, ok := get["firstID"]; ok {
+
+ s.FirstID = parseUInt64(val)
+ }
+ if val, ok := get["lastID"]; ok {
+
+ s.LastID = parseUInt64(val)
+ }
+ if val, ok := get["query"]; ok {
+
+ s.Query = val
+ }
+
+ return err
+}
+
+var _ RequestFiller = NewSearchMessages()
diff --git a/sam/rest/router.go b/sam/rest/router.go
index f1dfd9289..43aeeeda4 100644
--- a/sam/rest/router.go
+++ b/sam/rest/router.go
@@ -23,6 +23,7 @@ func MountRoutes() func(chi.Router) {
handlers.NewOrganisation(Organisation{}.New()).MountRoutes(r)
handlers.NewTeam(Team{}.New()).MountRoutes(r)
handlers.NewUser(User{}.New()).MountRoutes(r)
+ handlers.NewSearch(Search{}.New()).MountRoutes(r)
})
}
}
diff --git a/sam/rest/search.go b/sam/rest/search.go
new file mode 100644
index 000000000..84d5fa9f9
--- /dev/null
+++ b/sam/rest/search.go
@@ -0,0 +1,48 @@
+package rest
+
+import (
+ "context"
+
+ "github.com/crusttech/crust/internal/payload"
+ "github.com/crusttech/crust/internal/payload/outgoing"
+ "github.com/crusttech/crust/sam/rest/request"
+ "github.com/crusttech/crust/sam/service"
+ "github.com/crusttech/crust/sam/types"
+
+ "github.com/pkg/errors"
+)
+
+var _ = errors.Wrap
+
+type Search struct {
+ svc struct {
+ msg service.MessageService
+ }
+}
+
+func (Search) New() *Search {
+ ctrl := &Search{}
+ ctrl.svc.msg = service.DefaultMessage
+ return ctrl
+
+}
+
+func (ctrl *Search) Messages(ctx context.Context, r *request.SearchMessages) (interface{}, error) {
+ return ctrl.wrapSet(ctx)(ctrl.svc.msg.With(ctx).Find(&types.MessageFilter{
+ Query: r.Query,
+ ChannelID: r.InChannel,
+ UserID: r.FromUser,
+ FirstID: r.FirstID,
+ LastID: r.LastID,
+ }))
+}
+
+func (ctrl *Search) wrapSet(ctx context.Context) func(mm types.MessageSet, err error) (*outgoing.MessageSet, error) {
+ return func(mm types.MessageSet, err error) (*outgoing.MessageSet, error) {
+ if err != nil {
+ return nil, err
+ } else {
+ return payload.Messages(ctx, mm), nil
+ }
+ }
+}
diff --git a/sam/types/message.go b/sam/types/message.go
index 021e00d14..374387fb0 100644
--- a/sam/types/message.go
+++ b/sam/types/message.go
@@ -34,12 +34,15 @@ type (
// All messages that belong to a channel
ChannelID uint64
+ // Only messages that belong to a user
+ UserID uint64
+
// Return all replies to a single message
RepliesTo uint64
// (FirstID...LastID), for paging
//
- // Include all messsages which IDs range from "first" to "last" (exclusive!)
+ // Include all messages which IDs range from "first" to "last" (exclusive!)
FirstID uint64
LastID uint64