Move messages search rest endpoint under /search/messages, add controller, fix searching by query
This commit is contained in:
@@ -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<br><br>Values:<br><br><ul><li>`history`</li><li>`message`</li><li>`attachment`</li><li>`media`</li> | 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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
{
|
||||
"name": "state",
|
||||
"required": true,
|
||||
"title": "Valid values: delete, restore, archive, unarchive",
|
||||
"title": "Valid values: delete, undelete, archive, unarchive",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
|
||||
58
sam/docs/src/spec/search.json
Normal file
58
sam/docs/src/spec/search.json
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
57
sam/rest/handlers/search.go
Normal file
57
sam/rest/handlers/search.go
Normal file
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
97
sam/rest/request/search.go
Normal file
97
sam/rest/request/search.go
Normal file
@@ -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()
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
48
sam/rest/search.go
Normal file
48
sam/rest/search.go
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user