From d21d77576483ca178e40c5fea81b99faf2f23b87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Fri, 20 Sep 2024 20:37:55 +0200 Subject: [PATCH] Add ListSenderNames api route and use params.Add(..) in Filter classes --- flutter/lib/api/api_client.dart | 30 +++-- flutter/lib/models/api_error.dart | 2 +- .../lib/pages/debug/debug_request_view.dart | 1 + .../lib/pages/message_list/message_list.dart | 2 +- .../lib/pages/message_view/message_view.dart | 1 + scnserver/api/handler/apiMessage.go | 11 +- scnserver/models/messagefilter.go | 105 +++++++-------- scnserver/models/subscriptionfilter.go | 38 ++---- scnserver/test/message_test.go | 122 +++++++++++++++++- scnserver/test/util/factory.go | 2 +- 10 files changed, 209 insertions(+), 105 deletions(-) diff --git a/flutter/lib/api/api_client.dart b/flutter/lib/api/api_client.dart index 7dd11b0..8e53fbf 100644 --- a/flutter/lib/api/api_client.dart +++ b/flutter/lib/api/api_client.dart @@ -29,12 +29,13 @@ enum ChannelSelector { class MessageFilter { List? channelIDs; - String? searchFilter; + List? searchFilter; List? senderNames; List? usedKeys; List? priority; DateTime? timeBefore; DateTime? timeAfter; + bool? hasSenderName; MessageFilter({ this.channelIDs, @@ -54,7 +55,7 @@ class APIClient { required String name, required String method, required String relURL, - Map? query, + Map>? query, required T Function(Map json)? fn, dynamic jsonBody, String? authToken, @@ -66,7 +67,7 @@ class APIClient { final req = http.Request(method, uri); - print('[REQUEST|RUN] [${method}] ${name}'); + print('[REQUEST|RUN] [${method}] ${name} | ${uri.toString()}'); if (jsonBody != null) { req.body = jsonEncode(jsonBody); @@ -206,7 +207,9 @@ class APIClient { name: 'getChannelList', method: 'GET', relURL: 'users/${auth.getUserID()}/channels', - query: {'selector': sel.apiKey}, + query: { + 'selector': [sel.apiKey] + }, fn: (json) => ChannelWithSubscription.fromJsonArray(json['channels'] as List), authToken: auth.getToken(), ); @@ -252,14 +255,16 @@ class APIClient { method: 'GET', relURL: 'messages', query: { - 'next_page_token': pageToken, - if (pageSize != null) 'page_size': pageSize.toString(), - if (filter?.channelIDs != null) 'channel_id': filter!.channelIDs!.join(","), - if (filter?.senderNames != null) 'sender': filter!.senderNames!.join(","), - if (filter?.timeBefore != null) 'before': filter!.timeBefore!.toIso8601String(), - if (filter?.timeAfter != null) 'after': filter!.timeAfter!.toIso8601String(), - if (filter?.priority != null) 'priority': filter!.priority!.map((p) => p.toString()).join(","), - if (filter?.usedKeys != null) 'used_key': filter!.usedKeys!.join(","), + 'next_page_token': [pageToken], + if (pageSize != null) 'page_size': [pageSize.toString()], + if (filter?.searchFilter != null) 'search': filter!.searchFilter!, + if (filter?.channelIDs != null) 'channel_id': filter!.channelIDs!, + if (filter?.senderNames != null) 'sender': filter!.senderNames!, + if (filter?.hasSenderName != null) 'has_sender': [filter!.hasSenderName!.toString()], + if (filter?.timeBefore != null) 'before': [filter!.timeBefore!.toIso8601String()], + if (filter?.timeAfter != null) 'after': [filter!.timeAfter!.toIso8601String()], + if (filter?.priority != null) 'priority': filter!.priority!.map((p) => p.toString()).toList(), + if (filter?.usedKeys != null) 'used_key': filter!.usedKeys!, }, fn: (json) => SCNMessage.fromPaginatedJsonArray(json, 'messages', 'next_page_token'), authToken: auth.getToken(), @@ -271,7 +276,6 @@ class APIClient { name: 'getMessage', method: 'GET', relURL: 'messages/$msgid', - query: {}, fn: SCNMessage.fromJson, authToken: auth.getToken(), ); diff --git a/flutter/lib/models/api_error.dart b/flutter/lib/models/api_error.dart index 510d085..14f8ea0 100644 --- a/flutter/lib/models/api_error.dart +++ b/flutter/lib/models/api_error.dart @@ -66,7 +66,7 @@ class APIError { factory APIError.fromJson(Map json) { return APIError( success: json['success'] as bool, - error: (json['error'] as double).toInt(), + error: (json['error'] as num).toInt(), errhighlight: json['errhighlight'] as String, message: json['message'] as String, ); diff --git a/flutter/lib/pages/debug/debug_request_view.dart b/flutter/lib/pages/debug/debug_request_view.dart index 9e5d804..695b292 100644 --- a/flutter/lib/pages/debug/debug_request_view.dart +++ b/flutter/lib/pages/debug/debug_request_view.dart @@ -60,6 +60,7 @@ class DebugRequestViewPage extends StatelessWidget { onPressed: () { Clipboard.setData(new ClipboardData(text: title)); Toaster.info("Clipboard", 'Copied text to Clipboard'); + print('================= [CLIPBOARD] =================\n${title}\n================= [/CLIPBOARD] ================='); }, icon: FontAwesomeIcons.copy, ), diff --git a/flutter/lib/pages/message_list/message_list.dart b/flutter/lib/pages/message_list/message_list.dart index eaae496..708c521 100644 --- a/flutter/lib/pages/message_list/message_list.dart +++ b/flutter/lib/pages/message_list/message_list.dart @@ -308,7 +308,7 @@ class _MessageListPageState extends State with RouteAware { var chipletsSearch = _filterChiplets.where((p) => p.type == MessageFilterChipletType.search).toList(); if (chipletsSearch.isNotEmpty) { - filter.searchFilter = chipletsSearch.map((p) => p.value as String).first; + filter.searchFilter = chipletsSearch.map((p) => p.value as String).toList(); } var chipletsKeyTokens = _filterChiplets.where((p) => p.type == MessageFilterChipletType.sendkey).toList(); diff --git a/flutter/lib/pages/message_view/message_view.dart b/flutter/lib/pages/message_view/message_view.dart index 57f8a2c..d38e632 100644 --- a/flutter/lib/pages/message_view/message_view.dart +++ b/flutter/lib/pages/message_view/message_view.dart @@ -239,6 +239,7 @@ class _MessageViewPageState extends State { onPressed: () { Clipboard.setData(new ClipboardData(text: message.content ?? '')); Toaster.info("Clipboard", 'Copied text to Clipboard'); + print('================= [CLIPBOARD] =================\n${message.content}\n================= [/CLIPBOARD] ================='); }, icon: FontAwesomeIcons.copy, ), diff --git a/scnserver/api/handler/apiMessage.go b/scnserver/api/handler/apiMessage.go index 5a69749..7e2a7ad 100644 --- a/scnserver/api/handler/apiMessage.go +++ b/scnserver/api/handler/apiMessage.go @@ -39,7 +39,8 @@ func (h APIHandler) ListMessages(pctx ginext.PreContext) ginext.HTTPResponse { type query struct { PageSize *int `json:"page_size" form:"page_size"` NextPageToken *string `json:"next_page_token" form:"next_page_token"` - Filter *string `json:"filter" form:"filter"` + Search []string `json:"search" form:"search"` + StringSearch []string `json:"string_search" form:"string_search"` Trimmed *bool `json:"trimmed" form:"trimmed"` Channels []string `json:"channel" form:"channel"` ChannelIDs []string `json:"channel_id" form:"channel_id"` @@ -92,8 +93,12 @@ func (h APIHandler) ListMessages(pctx ginext.PreContext) ginext.HTTPResponse { ConfirmedSubscriptionBy: langext.Ptr(userid), } - if q.Filter != nil && strings.TrimSpace(*q.Filter) != "" { - filter.SearchString = langext.Ptr([]string{strings.TrimSpace(*q.Filter)}) + if len(q.Search) != 0 { + filter.SearchStringFTS = langext.Ptr(langext.ArrMap(q.Search, func(v string) string { return strings.TrimSpace(v) })) + } + + if len(q.StringSearch) != 0 { + filter.SearchStringPlain = langext.Ptr(langext.ArrMap(q.StringSearch, func(v string) string { return strings.TrimSpace(v) })) } if len(q.Channels) != 0 { diff --git a/scnserver/models/messagefilter.go b/scnserver/models/messagefilter.go index c27894b..158cf6a 100644 --- a/scnserver/models/messagefilter.go +++ b/scnserver/models/messagefilter.go @@ -15,7 +15,8 @@ import ( type MessageFilter struct { ConfirmedSubscriptionBy *UserID - SearchString *[]string + SearchStringFTS *[]string + SearchStringPlain *[]string Sender *[]UserID ChannelNameCS *[]string // case-sensitive ChannelNameCI *[]string // case-insensitive @@ -49,7 +50,7 @@ func (f MessageFilter) SQL() (string, string, sq.PP, error) { if f.ConfirmedSubscriptionBy != nil { joinClause += " LEFT JOIN subscriptions AS subs on messages.channel_id = subs.channel_id " } - if f.SearchString != nil { + if f.SearchStringFTS != nil { joinClause += " JOIN messages_fts AS mfts on (mfts.rowid = messages.rowid) " } @@ -66,60 +67,53 @@ func (f MessageFilter) SQL() (string, string, sq.PP, error) { } if f.ConfirmedSubscriptionBy != nil { - sqlClauses = append(sqlClauses, "(subs.subscriber_user_id = :sub_uid AND subs.confirmed = 1)") - params["sub_uid"] = *f.ConfirmedSubscriptionBy + sqlClauses = append(sqlClauses, fmt.Sprintf("(subs.subscriber_user_id = :%s AND subs.confirmed = 1)", params.Add(*f.ConfirmedSubscriptionBy))) } if f.Sender != nil { filter := make([]string, 0) - for i, v := range *f.Sender { - filter = append(filter, fmt.Sprintf("(sender_user_id = :sender_%d)", i)) - params[fmt.Sprintf("sender_%d", i)] = v + for _, v := range *f.Sender { + filter = append(filter, fmt.Sprintf("(sender_user_id = :%s)", params.Add(v))) } sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") } if f.ChannelNameCI != nil { filter := make([]string, 0) - for i, v := range *f.ChannelNameCI { - filter = append(filter, fmt.Sprintf("(messages.channel_internal_name = :channelnameci_%d COLLATE NOCASE)", i)) - params[fmt.Sprintf("channelnameci_%d", i)] = v + for _, v := range *f.ChannelNameCI { + filter = append(filter, fmt.Sprintf("(messages.channel_internal_name = :%s COLLATE NOCASE)", params.Add(v))) } sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") } if f.ChannelNameCS != nil { filter := make([]string, 0) - for i, v := range *f.ChannelNameCS { - filter = append(filter, fmt.Sprintf("(messages.channel_internal_name = :channelnamecs_%d COLLATE BINARY)", i)) - params[fmt.Sprintf("channelnamecs_%d", i)] = v + for _, v := range *f.ChannelNameCS { + filter = append(filter, fmt.Sprintf("(messages.channel_internal_name = :%s COLLATE BINARY)", params.Add(v))) } sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") } if f.ChannelID != nil { filter := make([]string, 0) - for i, v := range *f.ChannelID { - filter = append(filter, fmt.Sprintf("(messages.channel_id = :channelid_%d)", i)) - params[fmt.Sprintf("channelid_%d", i)] = v + for _, v := range *f.ChannelID { + filter = append(filter, fmt.Sprintf("(messages.channel_id = :%s)", params.Add(v))) } sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") } if f.SenderNameCI != nil { filter := make([]string, 0) - for i, v := range *f.SenderNameCI { - filter = append(filter, fmt.Sprintf("(sender_name = :sendernameci_%d COLLATE NOCASE)", i)) - params[fmt.Sprintf("sendernameci_%d", i)] = v + for _, v := range *f.SenderNameCI { + filter = append(filter, fmt.Sprintf("(sender_name = :%s COLLATE NOCASE)", params.Add(v))) } sqlClauses = append(sqlClauses, "(sender_name IS NOT NULL AND ("+strings.Join(filter, " OR ")+"))") } if f.SenderNameCS != nil { filter := make([]string, 0) - for i, v := range *f.SenderNameCS { - filter = append(filter, fmt.Sprintf("(sender_name = :sendernamecs_%d COLLATE BINARY)", i)) - params[fmt.Sprintf("sendernamecs_%d", i)] = v + for _, v := range *f.SenderNameCS { + filter = append(filter, fmt.Sprintf("(sender_name = :%s COLLATE BINARY)", params.Add(v))) } sqlClauses = append(sqlClauses, "(sender_name IS NOT NULL AND ("+strings.Join(filter, " OR ")+"))") } @@ -134,66 +128,54 @@ func (f MessageFilter) SQL() (string, string, sq.PP, error) { if f.SenderIP != nil { filter := make([]string, 0) - for i, v := range *f.SenderIP { - filter = append(filter, fmt.Sprintf("(sender_ip = :senderip_%d)", i)) - params[fmt.Sprintf("senderip_%d", i)] = v + for _, v := range *f.SenderIP { + filter = append(filter, fmt.Sprintf("(sender_ip = :%s)", params.Add(v))) } sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") } if f.TimestampCoalesce != nil { - sqlClauses = append(sqlClauses, "(COALESCE(timestamp_client, timestamp_real) = :ts_equals)") - params["ts_equals"] = (*f.TimestampCoalesce).UnixMilli() + sqlClauses = append(sqlClauses, fmt.Sprintf("(COALESCE(timestamp_client, timestamp_real) = :%s)", params.Add((*f.TimestampCoalesce).UnixMilli()))) } if f.TimestampCoalesceAfter != nil { - sqlClauses = append(sqlClauses, "(COALESCE(timestamp_client, timestamp_real) > :ts_after)") - params["ts_after"] = (*f.TimestampCoalesceAfter).UnixMilli() + sqlClauses = append(sqlClauses, fmt.Sprintf("(COALESCE(timestamp_client, timestamp_real) > :%s)", params.Add((*f.TimestampCoalesceAfter).UnixMilli()))) } if f.TimestampCoalesceBefore != nil { - sqlClauses = append(sqlClauses, "(COALESCE(timestamp_client, timestamp_real) < :ts_before)") - params["ts_before"] = (*f.TimestampCoalesceBefore).UnixMilli() + sqlClauses = append(sqlClauses, fmt.Sprintf("(COALESCE(timestamp_client, timestamp_real) < :%s)", params.Add((*f.TimestampCoalesceBefore).UnixMilli()))) } if f.TimestampReal != nil { - sqlClauses = append(sqlClauses, "(timestamp_real = :ts_real_equals)") - params["ts_real_equals"] = (*f.TimestampRealAfter).UnixMilli() + sqlClauses = append(sqlClauses, fmt.Sprintf("(timestamp_real = :%s)", params.Add((*f.TimestampRealAfter).UnixMilli()))) } if f.TimestampRealAfter != nil { - sqlClauses = append(sqlClauses, "(timestamp_real > :ts_real_after)") - params["ts_real_after"] = (*f.TimestampRealAfter).UnixMilli() + sqlClauses = append(sqlClauses, fmt.Sprintf("(timestamp_real > :%s)", params.Add((*f.TimestampRealAfter).UnixMilli()))) } if f.TimestampRealBefore != nil { - sqlClauses = append(sqlClauses, "(timestamp_real < :ts_real_before)") - params["ts_real_before"] = (*f.TimestampRealBefore).UnixMilli() + sqlClauses = append(sqlClauses, fmt.Sprintf("(timestamp_real < :%s)", params.Add((*f.TimestampRealBefore).UnixMilli()))) } if f.TimestampClient != nil { - sqlClauses = append(sqlClauses, "(timestamp_client IS NOT NULL AND timestamp_client = :ts_client_equals)") - params["ts_client_equals"] = (*f.TimestampClient).UnixMilli() + sqlClauses = append(sqlClauses, fmt.Sprintf("(timestamp_client IS NOT NULL AND timestamp_client = :%s)", params.Add((*f.TimestampClient).UnixMilli()))) } if f.TimestampClientAfter != nil { - sqlClauses = append(sqlClauses, "(timestamp_client IS NOT NULL AND timestamp_client > :ts_client_after)") - params["ts_client_after"] = (*f.TimestampClientAfter).UnixMilli() + sqlClauses = append(sqlClauses, fmt.Sprintf("(timestamp_client IS NOT NULL AND timestamp_client > :%s)", params.Add((*f.TimestampClientAfter).UnixMilli()))) } if f.TimestampClientBefore != nil { - sqlClauses = append(sqlClauses, "(timestamp_client IS NOT NULL AND timestamp_client < :ts_client_before)") - params["ts_client_before"] = (*f.TimestampClientBefore).UnixMilli() + sqlClauses = append(sqlClauses, fmt.Sprintf("(timestamp_client IS NOT NULL AND timestamp_client < :%s)", params.Add((*f.TimestampClientBefore).UnixMilli()))) } if f.TitleCI != nil { - sqlClauses = append(sqlClauses, "(title = :titleci COLLATE NOCASE)") - params["titleci"] = *f.TitleCI + sqlClauses = append(sqlClauses, fmt.Sprintf("(title = :%s COLLATE NOCASE)", params.Add(*f.TitleCI))) } if f.TitleCS != nil { - sqlClauses = append(sqlClauses, "(title = :titleci COLLATE BINARY)") - params["titleci"] = *f.TitleCI + sqlClauses = append(sqlClauses, fmt.Sprintf("(title = :%s COLLATE BINARY)", params.Add(*f.TitleCI))) } if f.Priority != nil { @@ -203,9 +185,8 @@ func (f MessageFilter) SQL() (string, string, sq.PP, error) { if f.UserMessageID != nil { filter := make([]string, 0) - for i, v := range *f.UserMessageID { - filter = append(filter, fmt.Sprintf("(usr_message_id = :usermessageid_%d)", i)) - params[fmt.Sprintf("usermessageid_%d", i)] = v + for _, v := range *f.UserMessageID { + filter = append(filter, fmt.Sprintf("(usr_message_id = :%s)", params.Add(v))) } sqlClauses = append(sqlClauses, "(usr_message_id IS NOT NULL AND ("+strings.Join(filter, " OR ")+"))") } @@ -222,18 +203,28 @@ func (f MessageFilter) SQL() (string, string, sq.PP, error) { if f.UsedKeyID != nil { filter := make([]string, 0) - for i, v := range *f.UsedKeyID { - filter = append(filter, fmt.Sprintf("(used_key_id = :usedkeyid_%d)", i)) - params[fmt.Sprintf("usedkeyid_%d", i)] = v + for _, v := range *f.UsedKeyID { + filter = append(filter, fmt.Sprintf("(used_key_id = :%s)", params.Add(v))) } sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") } - if f.SearchString != nil { + if f.SearchStringFTS != nil { filter := make([]string, 0) - for i, v := range *f.SearchString { - filter = append(filter, fmt.Sprintf("(messages_fts match :searchstring_%d)", i)) - params[fmt.Sprintf("searchstring_%d", i)] = v + for _, v := range *f.SearchStringFTS { + filter = append(filter, fmt.Sprintf("(messages_fts match :%s)", params.Add(v))) + } + sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") + } + + if f.SearchStringPlain != nil { + filter := make([]string, 0) + for _, v := range *f.SearchStringPlain { + filter = append(filter, fmt.Sprintf("instr(lower(messages.channel_internal_name), lower(:%s))", params.Add(v))) + filter = append(filter, fmt.Sprintf("instr(lower(messages.sender_name), lower(:%s))", params.Add(v))) + filter = append(filter, fmt.Sprintf("instr(lower(messages.title), lower(:%s))", params.Add(v))) + filter = append(filter, fmt.Sprintf("instr(lower(messages.content), lower(:%s))", params.Add(v))) + } sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") } diff --git a/scnserver/models/subscriptionfilter.go b/scnserver/models/subscriptionfilter.go index ccb5480..647f28f 100644 --- a/scnserver/models/subscriptionfilter.go +++ b/scnserver/models/subscriptionfilter.go @@ -34,52 +34,45 @@ func (f SubscriptionFilter) SQL() (string, string, sq.PP, error) { params := sq.PP{} if f.AnyUserID != nil { - sqlClauses = append(sqlClauses, "(subscriber_user_id = :anyuid1 OR channel_owner_user_id = :anyuid2)") - params["anyuid1"] = *f.AnyUserID - params["anyuid2"] = *f.AnyUserID + sqlClauses = append(sqlClauses, fmt.Sprintf("(subscriber_user_id = :%s OR channel_owner_user_id = :%s)", params.Add(*f.AnyUserID), params.Add(*f.AnyUserID))) } if f.SubscriberUserID != nil { filter := make([]string, 0) - for i, v := range *f.SubscriberUserID { - filter = append(filter, fmt.Sprintf("(subscriber_user_id = :subscriber_uid_1_%d)", i)) - params[fmt.Sprintf("subscriber_uid_1_%d", i)] = v + for _, v := range *f.SubscriberUserID { + filter = append(filter, fmt.Sprintf("(subscriber_user_id = :%s)", params.Add(v))) } sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") } if f.SubscriberUserID2 != nil { filter := make([]string, 0) - for i, v := range *f.SubscriberUserID2 { - filter = append(filter, fmt.Sprintf("(subscriber_user_id = :subscriber_uid_2_%d)", i)) - params[fmt.Sprintf("subscriber_uid_2_%d", i)] = v + for _, v := range *f.SubscriberUserID2 { + filter = append(filter, fmt.Sprintf("(subscriber_user_id = :%s)", params.Add(v))) } sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") } if f.ChannelOwnerUserID != nil { filter := make([]string, 0) - for i, v := range *f.ChannelOwnerUserID { - filter = append(filter, fmt.Sprintf("(channel_owner_user_id = :chanowner_uid_1_%d)", i)) - params[fmt.Sprintf("chanowner_uid_1_%d", i)] = v + for _, v := range *f.ChannelOwnerUserID { + filter = append(filter, fmt.Sprintf("(channel_owner_user_id = :%s)", params.Add(v))) } sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") } if f.ChannelOwnerUserID2 != nil { filter := make([]string, 0) - for i, v := range *f.ChannelOwnerUserID2 { - filter = append(filter, fmt.Sprintf("(channel_owner_user_id = :chanowner_uid_2_%d)", i)) - params[fmt.Sprintf("chanowner_uid_2_%d", i)] = v + for _, v := range *f.ChannelOwnerUserID2 { + filter = append(filter, fmt.Sprintf("(channel_owner_user_id = :%s)", params.Add(v))) } sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") } if f.ChannelID != nil { filter := make([]string, 0) - for i, v := range *f.ChannelID { - filter = append(filter, fmt.Sprintf("(channel_id = :chanid_%d)", i)) - params[fmt.Sprintf("chanid_%d", i)] = v + for _, v := range *f.ChannelID { + filter = append(filter, fmt.Sprintf("(channel_id = :%s)", params.Add(v))) } sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") } @@ -101,18 +94,15 @@ func (f SubscriptionFilter) SQL() (string, string, sq.PP, error) { } if f.Timestamp != nil { - sqlClauses = append(sqlClauses, "(timestamp_created = :ts_equals)") - params["ts_equals"] = (*f.Timestamp).UnixMilli() + sqlClauses = append(sqlClauses, fmt.Sprintf("(timestamp_created = :%s)", params.Add((*f.Timestamp).UnixMilli()))) } if f.TimestampAfter != nil { - sqlClauses = append(sqlClauses, "(timestamp_created > :ts_after)") - params["ts_after"] = (*f.TimestampAfter).UnixMilli() + sqlClauses = append(sqlClauses, fmt.Sprintf("(timestamp_created > :%s)", params.Add((*f.TimestampAfter).UnixMilli()))) } if f.TimestampBefore != nil { - sqlClauses = append(sqlClauses, "(timestamp_created < :ts_before)") - params["ts_before"] = (*f.TimestampBefore).UnixMilli() + sqlClauses = append(sqlClauses, fmt.Sprintf("(timestamp_created < ::%s)", params.Add((*f.TimestampBefore).UnixMilli()))) } sqlClause := "" diff --git a/scnserver/test/message_test.go b/scnserver/test/message_test.go index f550828..807bb24 100644 --- a/scnserver/test/message_test.go +++ b/scnserver/test/message_test.go @@ -38,7 +38,7 @@ func TestSearchMessageFTSSimple(t *testing.T) { Messages []msg `json:"messages"` } - msgList := tt.RequestAuthGet[mglist](t, data.User[0].AdminKey, baseUrl, fmt.Sprintf("/api/v2/messages?filter=%s", url.QueryEscape("Friday"))) + msgList := tt.RequestAuthGet[mglist](t, data.User[0].AdminKey, baseUrl, fmt.Sprintf("/api/v2/messages?search=%s", url.QueryEscape("Friday"))) tt.AssertEqual(t, "msgList.len", 2, len(msgList.Messages)) tt.AssertArrAny(t, "msgList.any<1>", msgList.Messages, func(msg msg) bool { return msg.Title == "Invitation" }) tt.AssertArrAny(t, "msgList.any<2>", msgList.Messages, func(msg msg) bool { return msg.Title == "Important notice" }) @@ -101,7 +101,7 @@ func TestSearchMessageFTSMulti(t *testing.T) { } { - msgList := tt.RequestAuthGet[mglist](t, data.User[0].AdminKey, baseUrl, fmt.Sprintf("/api/v2/messages?used_key=%s&used_key=%s&channel=%s&filter=%s", skey, akey, "main", url.QueryEscape("tomorrow"))) + msgList := tt.RequestAuthGet[mglist](t, data.User[0].AdminKey, baseUrl, fmt.Sprintf("/api/v2/messages?used_key=%s&used_key=%s&channel=%s&search=%s", skey, akey, "main", url.QueryEscape("tomorrow"))) tt.AssertEqual(t, "msgList.len", 1, len(msgList.Messages)) tt.AssertEqual(t, "msg.Title", "Notice", msgList.Messages[0].Title) } @@ -699,7 +699,7 @@ func TestListMessagesZeroPagesize(t *testing.T) { tt.AssertEqual(t, "msgList[0]", "Lorem Ipsum 23", msgList1.Messages[0].Title) } -func TestListMessagesFilterChannel(t *testing.T) { +func TestListMessagesFiltered(t *testing.T) { ws, baseUrl, stop := tt.StartSimpleWebserver(t) defer stop() @@ -763,8 +763,8 @@ func TestListMessagesFilterChannel(t *testing.T) { {"channel=Reminders", 6, fmt.Sprintf("/api/v2/messages?channel=%s", "Reminders")}, {"channel_id=1", 6, fmt.Sprintf("/api/v2/messages?channel_id=%s", cid1)}, {"channel_id=1|2", 9, fmt.Sprintf("/api/v2/messages?channel_id=%s&channel_id=%s", cid1, cid2)}, - {"filter=unusual", 1, fmt.Sprintf("/api/v2/messages?filter=%s", "unusual")}, - {"filter=your", 6, fmt.Sprintf("/api/v2/messages?filter=%s", "your")}, + {"search=unusual", 1, fmt.Sprintf("/api/v2/messages?search=%s", "unusual")}, + {"search=your", 6, fmt.Sprintf("/api/v2/messages?search=%s", "your")}, {"prio=0", 5, fmt.Sprintf("/api/v2/messages?priority=%s", "0")}, {"prio=1", 4 + 7, fmt.Sprintf("/api/v2/messages?priority=%s", "1")}, {"prio=2", 6, fmt.Sprintf("/api/v2/messages?priority=%s", "2")}, @@ -790,3 +790,115 @@ func TestListMessagesFilterChannel(t *testing.T) { tt.AssertEqual(t, "msgList.filter["+testdata.Name+"].len", testdata.Count, msgList.TotalCount) } } + +func TestListMessagesSearch(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data := tt.InitDefaultData(t, ws) + + type msg struct { + ChannelId string `json:"channel_id"` + ChannelInternalName string `json:"channel_internal_name"` + Content string `json:"content"` + MessageId string `json:"message_id"` + OwnerUserId string `json:"owner_user_id"` + Priority int `json:"priority"` + SenderIp string `json:"sender_ip"` + SenderName string `json:"sender_name"` + SenderUserId string `json:"sender_user_id"` + Timestamp string `json:"timestamp"` + Title string `json:"title"` + Trimmed bool `json:"trimmed"` + UsrMessageId string `json:"usr_message_id"` + } + type mglist struct { + Messages []msg `json:"messages"` + TotalCount int `json:"total_count"` + } + + filterTests := []struct { + Name string + Count int + Query string + }{ + {"all", 22, fmt.Sprintf("/api/v2/messages")}, + + {"search=Promotions", 3, fmt.Sprintf("/api/v2/messages?search=%s", url.QueryEscape("Promotions"))}, + {"search=Important(1)", 3, fmt.Sprintf("/api/v2/messages?search=%s", url.QueryEscape("Important"))}, + {"search=Important(2)", 3, fmt.Sprintf("/api/v2/messages?search=%s", url.QueryEscape("important"))}, + {"search=Important(3)", 3, fmt.Sprintf("/api/v2/messages?search=%s", url.QueryEscape("IMPORTANT"))}, + {"search=safetyTraining(1)", 1, fmt.Sprintf("/api/v2/messages?search=%s", url.QueryEscape("safety training"))}, + {"search=safetyTraining(2)", 1, fmt.Sprintf("/api/v2/messages?search=%s", url.QueryEscape("\"safety training\""))}, + {"search=staffMeeting(1)", 2, fmt.Sprintf("/api/v2/messages?search=%s", url.QueryEscape("staff meeting"))}, + {"search=staffMeeting(2)", 1, fmt.Sprintf("/api/v2/messages?search=%s", url.QueryEscape("\"staff meeting\""))}, + {"search=?", 0, fmt.Sprintf("/api/v2/messages?search=%s", url.QueryEscape("\"?\""))}, // fails because FTS searches for full words + {"search=Prom", 0, fmt.Sprintf("/api/v2/messages?search=%s", url.QueryEscape("Prom"))}, // fails because FTS searches for full words + {"search=the(1)", 17, fmt.Sprintf("/api/v2/messages?search=%s", url.QueryEscape("the"))}, + {"search=the(2)", 17, fmt.Sprintf("/api/v2/messages?search=%s", url.QueryEscape("THE"))}, + {"search=please", 9, fmt.Sprintf("/api/v2/messages?search=%s", url.QueryEscape("please"))}, + {"search=11pm", 2, fmt.Sprintf("/api/v2/messages?search=%s", url.QueryEscape("\"11:00pm\""))}, + } + + for _, testdata := range filterTests { + msgList := tt.RequestAuthGet[mglist](t, data.User[0].AdminKey, baseUrl, testdata.Query) + tt.AssertEqual(t, "msgList.filter["+testdata.Name+"].len", testdata.Count, msgList.TotalCount) + } +} + +func TestListMessagesStringSearch(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data := tt.InitDefaultData(t, ws) + + type msg struct { + ChannelId string `json:"channel_id"` + ChannelInternalName string `json:"channel_internal_name"` + Content string `json:"content"` + MessageId string `json:"message_id"` + OwnerUserId string `json:"owner_user_id"` + Priority int `json:"priority"` + SenderIp string `json:"sender_ip"` + SenderName string `json:"sender_name"` + SenderUserId string `json:"sender_user_id"` + Timestamp string `json:"timestamp"` + Title string `json:"title"` + Trimmed bool `json:"trimmed"` + UsrMessageId string `json:"usr_message_id"` + } + type mglist struct { + Messages []msg `json:"messages"` + TotalCount int `json:"total_count"` + } + + filterTests := []struct { + Name string + Count int + Query string + }{ + {"all", 22, fmt.Sprintf("/api/v2/messages")}, + + {"search=Promotions", 3, fmt.Sprintf("/api/v2/messages?string_search=%s", url.QueryEscape("Promotions"))}, + {"search=Important(1)", 3, fmt.Sprintf("/api/v2/messages?string_search=%s", url.QueryEscape("Important"))}, + {"search=Important(2)", 3, fmt.Sprintf("/api/v2/messages?string_search=%s", url.QueryEscape("important"))}, + {"search=Important(3)", 3, fmt.Sprintf("/api/v2/messages?string_search=%s", url.QueryEscape("IMPORTANT"))}, + {"search=safetyTraining", 1, fmt.Sprintf("/api/v2/messages?string_search=%s", url.QueryEscape("safety training"))}, + {"search=?", 1, fmt.Sprintf("/api/v2/messages?string_search=%s", url.QueryEscape("?"))}, + {"search=the(1)", 17, fmt.Sprintf("/api/v2/messages?string_search=%s", url.QueryEscape("the"))}, + {"search=the(2)", 17, fmt.Sprintf("/api/v2/messages?string_search=%s", url.QueryEscape("THE"))}, + {"search=please", 9, fmt.Sprintf("/api/v2/messages?string_search=%s", url.QueryEscape("please"))}, + {"search=there", 3, fmt.Sprintf("/api/v2/messages?string_search=%s", url.QueryEscape("tHERe"))}, + {"search=11pm", 2, fmt.Sprintf("/api/v2/messages?string_search=%s", url.QueryEscape("11:00pm"))}, + + {"search=Prom", 3, fmt.Sprintf("/api/v2/messages?string_search=%s", url.QueryEscape("Prom"))}, + {"search=run", 1, fmt.Sprintf("/api/v2/messages?string_search=%s", url.QueryEscape("run"))}, + + {"search=please+there", 10, fmt.Sprintf("/api/v2/messages?string_search=%s&string_search=%s", url.QueryEscape("please"), url.QueryEscape("THERE"))}, + } + + for _, testdata := range filterTests { + msgList := tt.RequestAuthGet[mglist](t, data.User[0].AdminKey, baseUrl, testdata.Query) + tt.AssertEqual(t, "msgList.filter["+testdata.Name+"].len", testdata.Count, msgList.TotalCount) + } +} diff --git a/scnserver/test/util/factory.go b/scnserver/test/util/factory.go index 43a272b..3d649ca 100644 --- a/scnserver/test/util/factory.go +++ b/scnserver/test/util/factory.go @@ -139,7 +139,7 @@ var messageExamples = []msgex{ {0, "", "", P0, SKEY, "Deadline reminder", "Please remember to submit your project proposal by the end of the day \U0001f638", 0}, {0, "Reminders", "", PX, AKEY, "Attention - The copier is out of toner", "", 0}, {0, "Reminders", "Cellular Confidant", P2, SKEY, "Reminder", "Don't forget to clock in before starting your shift", timeext.FromHours(0.40)}, - {0, "Reminders", "Cellular Confidant", P1, AKEY, "Important", "There will be a company-wide meeting on Monday at 9:00am in the conference room", timeext.FromHours(23.15)}, + {0, "Reminders", "Cellular Confidant", P1, AKEY, "Important", "There will be a company-wide meeting on Monday at 9:00am in the conference room, all staff must attend", timeext.FromHours(23.15)}, {0, "", "", P2, SKEY, "System update", "We will be performing maintenance on the server tonight at 11:00pm. The system may be unavailable for up to an hour. Please save any unsaved work before then", 0}, {0, "Promotions", "Pocket Pal", P0, SKEY, "Attention - The first aid kit is running low on supplies.", "", 0}, {0, "Promotions", "Pocket Pal", PX, AKEY, "Urgent", "We have received a complaint about a safety hazard in the workplace. Please address the issue immediately", 0},