Add ListSenderNames api route and use params.Add(..) in Filter classes
Some checks failed
Build Docker and Deploy / Run Unit-Tests (push) Failing after 10s
Build Docker and Deploy / Build Docker Container (push) Successful in 1m14s
Build Docker and Deploy / Deploy to Server (push) Has been skipped

This commit is contained in:
Mike Schwörer 2024-09-20 20:37:55 +02:00
parent 352f1ca0d1
commit d21d775764
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
10 changed files with 209 additions and 105 deletions

View File

@ -29,12 +29,13 @@ enum ChannelSelector {
class MessageFilter { class MessageFilter {
List<String>? channelIDs; List<String>? channelIDs;
String? searchFilter; List<String>? searchFilter;
List<String>? senderNames; List<String>? senderNames;
List<String>? usedKeys; List<String>? usedKeys;
List<int>? priority; List<int>? priority;
DateTime? timeBefore; DateTime? timeBefore;
DateTime? timeAfter; DateTime? timeAfter;
bool? hasSenderName;
MessageFilter({ MessageFilter({
this.channelIDs, this.channelIDs,
@ -54,7 +55,7 @@ class APIClient {
required String name, required String name,
required String method, required String method,
required String relURL, required String relURL,
Map<String, String>? query, Map<String, Iterable<String>>? query,
required T Function(Map<String, dynamic> json)? fn, required T Function(Map<String, dynamic> json)? fn,
dynamic jsonBody, dynamic jsonBody,
String? authToken, String? authToken,
@ -66,7 +67,7 @@ class APIClient {
final req = http.Request(method, uri); final req = http.Request(method, uri);
print('[REQUEST|RUN] [${method}] ${name}'); print('[REQUEST|RUN] [${method}] ${name} | ${uri.toString()}');
if (jsonBody != null) { if (jsonBody != null) {
req.body = jsonEncode(jsonBody); req.body = jsonEncode(jsonBody);
@ -206,7 +207,9 @@ class APIClient {
name: 'getChannelList', name: 'getChannelList',
method: 'GET', method: 'GET',
relURL: 'users/${auth.getUserID()}/channels', relURL: 'users/${auth.getUserID()}/channels',
query: {'selector': sel.apiKey}, query: {
'selector': [sel.apiKey]
},
fn: (json) => ChannelWithSubscription.fromJsonArray(json['channels'] as List<dynamic>), fn: (json) => ChannelWithSubscription.fromJsonArray(json['channels'] as List<dynamic>),
authToken: auth.getToken(), authToken: auth.getToken(),
); );
@ -252,14 +255,16 @@ class APIClient {
method: 'GET', method: 'GET',
relURL: 'messages', relURL: 'messages',
query: { query: {
'next_page_token': pageToken, 'next_page_token': [pageToken],
if (pageSize != null) 'page_size': pageSize.toString(), if (pageSize != null) 'page_size': [pageSize.toString()],
if (filter?.channelIDs != null) 'channel_id': filter!.channelIDs!.join(","), if (filter?.searchFilter != null) 'search': filter!.searchFilter!,
if (filter?.senderNames != null) 'sender': filter!.senderNames!.join(","), if (filter?.channelIDs != null) 'channel_id': filter!.channelIDs!,
if (filter?.timeBefore != null) 'before': filter!.timeBefore!.toIso8601String(), if (filter?.senderNames != null) 'sender': filter!.senderNames!,
if (filter?.timeAfter != null) 'after': filter!.timeAfter!.toIso8601String(), if (filter?.hasSenderName != null) 'has_sender': [filter!.hasSenderName!.toString()],
if (filter?.priority != null) 'priority': filter!.priority!.map((p) => p.toString()).join(","), if (filter?.timeBefore != null) 'before': [filter!.timeBefore!.toIso8601String()],
if (filter?.usedKeys != null) 'used_key': filter!.usedKeys!.join(","), 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'), fn: (json) => SCNMessage.fromPaginatedJsonArray(json, 'messages', 'next_page_token'),
authToken: auth.getToken(), authToken: auth.getToken(),
@ -271,7 +276,6 @@ class APIClient {
name: 'getMessage', name: 'getMessage',
method: 'GET', method: 'GET',
relURL: 'messages/$msgid', relURL: 'messages/$msgid',
query: {},
fn: SCNMessage.fromJson, fn: SCNMessage.fromJson,
authToken: auth.getToken(), authToken: auth.getToken(),
); );

View File

@ -66,7 +66,7 @@ class APIError {
factory APIError.fromJson(Map<String, dynamic> json) { factory APIError.fromJson(Map<String, dynamic> json) {
return APIError( return APIError(
success: json['success'] as bool, success: json['success'] as bool,
error: (json['error'] as double).toInt(), error: (json['error'] as num).toInt(),
errhighlight: json['errhighlight'] as String, errhighlight: json['errhighlight'] as String,
message: json['message'] as String, message: json['message'] as String,
); );

View File

@ -60,6 +60,7 @@ class DebugRequestViewPage extends StatelessWidget {
onPressed: () { onPressed: () {
Clipboard.setData(new ClipboardData(text: title)); Clipboard.setData(new ClipboardData(text: title));
Toaster.info("Clipboard", 'Copied text to Clipboard'); Toaster.info("Clipboard", 'Copied text to Clipboard');
print('================= [CLIPBOARD] =================\n${title}\n================= [/CLIPBOARD] =================');
}, },
icon: FontAwesomeIcons.copy, icon: FontAwesomeIcons.copy,
), ),

View File

@ -308,7 +308,7 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
var chipletsSearch = _filterChiplets.where((p) => p.type == MessageFilterChipletType.search).toList(); var chipletsSearch = _filterChiplets.where((p) => p.type == MessageFilterChipletType.search).toList();
if (chipletsSearch.isNotEmpty) { 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(); var chipletsKeyTokens = _filterChiplets.where((p) => p.type == MessageFilterChipletType.sendkey).toList();

View File

@ -239,6 +239,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
onPressed: () { onPressed: () {
Clipboard.setData(new ClipboardData(text: message.content ?? '')); Clipboard.setData(new ClipboardData(text: message.content ?? ''));
Toaster.info("Clipboard", 'Copied text to Clipboard'); Toaster.info("Clipboard", 'Copied text to Clipboard');
print('================= [CLIPBOARD] =================\n${message.content}\n================= [/CLIPBOARD] =================');
}, },
icon: FontAwesomeIcons.copy, icon: FontAwesomeIcons.copy,
), ),

View File

@ -39,7 +39,8 @@ func (h APIHandler) ListMessages(pctx ginext.PreContext) ginext.HTTPResponse {
type query struct { type query struct {
PageSize *int `json:"page_size" form:"page_size"` PageSize *int `json:"page_size" form:"page_size"`
NextPageToken *string `json:"next_page_token" form:"next_page_token"` 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"` Trimmed *bool `json:"trimmed" form:"trimmed"`
Channels []string `json:"channel" form:"channel"` Channels []string `json:"channel" form:"channel"`
ChannelIDs []string `json:"channel_id" form:"channel_id"` 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), ConfirmedSubscriptionBy: langext.Ptr(userid),
} }
if q.Filter != nil && strings.TrimSpace(*q.Filter) != "" { if len(q.Search) != 0 {
filter.SearchString = langext.Ptr([]string{strings.TrimSpace(*q.Filter)}) 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 { if len(q.Channels) != 0 {

View File

@ -15,7 +15,8 @@ import (
type MessageFilter struct { type MessageFilter struct {
ConfirmedSubscriptionBy *UserID ConfirmedSubscriptionBy *UserID
SearchString *[]string SearchStringFTS *[]string
SearchStringPlain *[]string
Sender *[]UserID Sender *[]UserID
ChannelNameCS *[]string // case-sensitive ChannelNameCS *[]string // case-sensitive
ChannelNameCI *[]string // case-insensitive ChannelNameCI *[]string // case-insensitive
@ -49,7 +50,7 @@ func (f MessageFilter) SQL() (string, string, sq.PP, error) {
if f.ConfirmedSubscriptionBy != nil { if f.ConfirmedSubscriptionBy != nil {
joinClause += " LEFT JOIN subscriptions AS subs on messages.channel_id = subs.channel_id " 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) " 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 { if f.ConfirmedSubscriptionBy != nil {
sqlClauses = append(sqlClauses, "(subs.subscriber_user_id = :sub_uid AND subs.confirmed = 1)") sqlClauses = append(sqlClauses, fmt.Sprintf("(subs.subscriber_user_id = :%s AND subs.confirmed = 1)", params.Add(*f.ConfirmedSubscriptionBy)))
params["sub_uid"] = *f.ConfirmedSubscriptionBy
} }
if f.Sender != nil { if f.Sender != nil {
filter := make([]string, 0) filter := make([]string, 0)
for i, v := range *f.Sender { for _, v := range *f.Sender {
filter = append(filter, fmt.Sprintf("(sender_user_id = :sender_%d)", i)) filter = append(filter, fmt.Sprintf("(sender_user_id = :%s)", params.Add(v)))
params[fmt.Sprintf("sender_%d", i)] = v
} }
sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")")
} }
if f.ChannelNameCI != nil { if f.ChannelNameCI != nil {
filter := make([]string, 0) filter := make([]string, 0)
for i, v := range *f.ChannelNameCI { for _, v := range *f.ChannelNameCI {
filter = append(filter, fmt.Sprintf("(messages.channel_internal_name = :channelnameci_%d COLLATE NOCASE)", i)) filter = append(filter, fmt.Sprintf("(messages.channel_internal_name = :%s COLLATE NOCASE)", params.Add(v)))
params[fmt.Sprintf("channelnameci_%d", i)] = v
} }
sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")")
} }
if f.ChannelNameCS != nil { if f.ChannelNameCS != nil {
filter := make([]string, 0) filter := make([]string, 0)
for i, v := range *f.ChannelNameCS { for _, v := range *f.ChannelNameCS {
filter = append(filter, fmt.Sprintf("(messages.channel_internal_name = :channelnamecs_%d COLLATE BINARY)", i)) filter = append(filter, fmt.Sprintf("(messages.channel_internal_name = :%s COLLATE BINARY)", params.Add(v)))
params[fmt.Sprintf("channelnamecs_%d", i)] = v
} }
sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")")
} }
if f.ChannelID != nil { if f.ChannelID != nil {
filter := make([]string, 0) filter := make([]string, 0)
for i, v := range *f.ChannelID { for _, v := range *f.ChannelID {
filter = append(filter, fmt.Sprintf("(messages.channel_id = :channelid_%d)", i)) filter = append(filter, fmt.Sprintf("(messages.channel_id = :%s)", params.Add(v)))
params[fmt.Sprintf("channelid_%d", i)] = v
} }
sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")")
} }
if f.SenderNameCI != nil { if f.SenderNameCI != nil {
filter := make([]string, 0) filter := make([]string, 0)
for i, v := range *f.SenderNameCI { for _, v := range *f.SenderNameCI {
filter = append(filter, fmt.Sprintf("(sender_name = :sendernameci_%d COLLATE NOCASE)", i)) filter = append(filter, fmt.Sprintf("(sender_name = :%s COLLATE NOCASE)", params.Add(v)))
params[fmt.Sprintf("sendernameci_%d", i)] = v
} }
sqlClauses = append(sqlClauses, "(sender_name IS NOT NULL AND ("+strings.Join(filter, " OR ")+"))") sqlClauses = append(sqlClauses, "(sender_name IS NOT NULL AND ("+strings.Join(filter, " OR ")+"))")
} }
if f.SenderNameCS != nil { if f.SenderNameCS != nil {
filter := make([]string, 0) filter := make([]string, 0)
for i, v := range *f.SenderNameCS { for _, v := range *f.SenderNameCS {
filter = append(filter, fmt.Sprintf("(sender_name = :sendernamecs_%d COLLATE BINARY)", i)) filter = append(filter, fmt.Sprintf("(sender_name = :%s COLLATE BINARY)", params.Add(v)))
params[fmt.Sprintf("sendernamecs_%d", i)] = v
} }
sqlClauses = append(sqlClauses, "(sender_name IS NOT NULL AND ("+strings.Join(filter, " OR ")+"))") 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 { if f.SenderIP != nil {
filter := make([]string, 0) filter := make([]string, 0)
for i, v := range *f.SenderIP { for _, v := range *f.SenderIP {
filter = append(filter, fmt.Sprintf("(sender_ip = :senderip_%d)", i)) filter = append(filter, fmt.Sprintf("(sender_ip = :%s)", params.Add(v)))
params[fmt.Sprintf("senderip_%d", i)] = v
} }
sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")")
} }
if f.TimestampCoalesce != nil { if f.TimestampCoalesce != nil {
sqlClauses = append(sqlClauses, "(COALESCE(timestamp_client, timestamp_real) = :ts_equals)") sqlClauses = append(sqlClauses, fmt.Sprintf("(COALESCE(timestamp_client, timestamp_real) = :%s)", params.Add((*f.TimestampCoalesce).UnixMilli())))
params["ts_equals"] = (*f.TimestampCoalesce).UnixMilli()
} }
if f.TimestampCoalesceAfter != nil { if f.TimestampCoalesceAfter != nil {
sqlClauses = append(sqlClauses, "(COALESCE(timestamp_client, timestamp_real) > :ts_after)") sqlClauses = append(sqlClauses, fmt.Sprintf("(COALESCE(timestamp_client, timestamp_real) > :%s)", params.Add((*f.TimestampCoalesceAfter).UnixMilli())))
params["ts_after"] = (*f.TimestampCoalesceAfter).UnixMilli()
} }
if f.TimestampCoalesceBefore != nil { if f.TimestampCoalesceBefore != nil {
sqlClauses = append(sqlClauses, "(COALESCE(timestamp_client, timestamp_real) < :ts_before)") sqlClauses = append(sqlClauses, fmt.Sprintf("(COALESCE(timestamp_client, timestamp_real) < :%s)", params.Add((*f.TimestampCoalesceBefore).UnixMilli())))
params["ts_before"] = (*f.TimestampCoalesceBefore).UnixMilli()
} }
if f.TimestampReal != nil { if f.TimestampReal != nil {
sqlClauses = append(sqlClauses, "(timestamp_real = :ts_real_equals)") sqlClauses = append(sqlClauses, fmt.Sprintf("(timestamp_real = :%s)", params.Add((*f.TimestampRealAfter).UnixMilli())))
params["ts_real_equals"] = (*f.TimestampRealAfter).UnixMilli()
} }
if f.TimestampRealAfter != nil { if f.TimestampRealAfter != nil {
sqlClauses = append(sqlClauses, "(timestamp_real > :ts_real_after)") sqlClauses = append(sqlClauses, fmt.Sprintf("(timestamp_real > :%s)", params.Add((*f.TimestampRealAfter).UnixMilli())))
params["ts_real_after"] = (*f.TimestampRealAfter).UnixMilli()
} }
if f.TimestampRealBefore != nil { if f.TimestampRealBefore != nil {
sqlClauses = append(sqlClauses, "(timestamp_real < :ts_real_before)") sqlClauses = append(sqlClauses, fmt.Sprintf("(timestamp_real < :%s)", params.Add((*f.TimestampRealBefore).UnixMilli())))
params["ts_real_before"] = (*f.TimestampRealBefore).UnixMilli()
} }
if f.TimestampClient != nil { if f.TimestampClient != nil {
sqlClauses = append(sqlClauses, "(timestamp_client IS NOT NULL AND timestamp_client = :ts_client_equals)") sqlClauses = append(sqlClauses, fmt.Sprintf("(timestamp_client IS NOT NULL AND timestamp_client = :%s)", params.Add((*f.TimestampClient).UnixMilli())))
params["ts_client_equals"] = (*f.TimestampClient).UnixMilli()
} }
if f.TimestampClientAfter != nil { if f.TimestampClientAfter != nil {
sqlClauses = append(sqlClauses, "(timestamp_client IS NOT NULL AND timestamp_client > :ts_client_after)") sqlClauses = append(sqlClauses, fmt.Sprintf("(timestamp_client IS NOT NULL AND timestamp_client > :%s)", params.Add((*f.TimestampClientAfter).UnixMilli())))
params["ts_client_after"] = (*f.TimestampClientAfter).UnixMilli()
} }
if f.TimestampClientBefore != nil { if f.TimestampClientBefore != nil {
sqlClauses = append(sqlClauses, "(timestamp_client IS NOT NULL AND timestamp_client < :ts_client_before)") sqlClauses = append(sqlClauses, fmt.Sprintf("(timestamp_client IS NOT NULL AND timestamp_client < :%s)", params.Add((*f.TimestampClientBefore).UnixMilli())))
params["ts_client_before"] = (*f.TimestampClientBefore).UnixMilli()
} }
if f.TitleCI != nil { if f.TitleCI != nil {
sqlClauses = append(sqlClauses, "(title = :titleci COLLATE NOCASE)") sqlClauses = append(sqlClauses, fmt.Sprintf("(title = :%s COLLATE NOCASE)", params.Add(*f.TitleCI)))
params["titleci"] = *f.TitleCI
} }
if f.TitleCS != nil { if f.TitleCS != nil {
sqlClauses = append(sqlClauses, "(title = :titleci COLLATE BINARY)") sqlClauses = append(sqlClauses, fmt.Sprintf("(title = :%s COLLATE BINARY)", params.Add(*f.TitleCI)))
params["titleci"] = *f.TitleCI
} }
if f.Priority != nil { if f.Priority != nil {
@ -203,9 +185,8 @@ func (f MessageFilter) SQL() (string, string, sq.PP, error) {
if f.UserMessageID != nil { if f.UserMessageID != nil {
filter := make([]string, 0) filter := make([]string, 0)
for i, v := range *f.UserMessageID { for _, v := range *f.UserMessageID {
filter = append(filter, fmt.Sprintf("(usr_message_id = :usermessageid_%d)", i)) filter = append(filter, fmt.Sprintf("(usr_message_id = :%s)", params.Add(v)))
params[fmt.Sprintf("usermessageid_%d", i)] = v
} }
sqlClauses = append(sqlClauses, "(usr_message_id IS NOT NULL AND ("+strings.Join(filter, " OR ")+"))") 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 { if f.UsedKeyID != nil {
filter := make([]string, 0) filter := make([]string, 0)
for i, v := range *f.UsedKeyID { for _, v := range *f.UsedKeyID {
filter = append(filter, fmt.Sprintf("(used_key_id = :usedkeyid_%d)", i)) filter = append(filter, fmt.Sprintf("(used_key_id = :%s)", params.Add(v)))
params[fmt.Sprintf("usedkeyid_%d", i)] = v
} }
sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")")
} }
if f.SearchString != nil { if f.SearchStringFTS != nil {
filter := make([]string, 0) filter := make([]string, 0)
for i, v := range *f.SearchString { for _, v := range *f.SearchStringFTS {
filter = append(filter, fmt.Sprintf("(messages_fts match :searchstring_%d)", i)) filter = append(filter, fmt.Sprintf("(messages_fts match :%s)", params.Add(v)))
params[fmt.Sprintf("searchstring_%d", i)] = 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 ")+")") sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")")
} }

View File

@ -34,52 +34,45 @@ func (f SubscriptionFilter) SQL() (string, string, sq.PP, error) {
params := sq.PP{} params := sq.PP{}
if f.AnyUserID != nil { if f.AnyUserID != nil {
sqlClauses = append(sqlClauses, "(subscriber_user_id = :anyuid1 OR channel_owner_user_id = :anyuid2)") sqlClauses = append(sqlClauses, fmt.Sprintf("(subscriber_user_id = :%s OR channel_owner_user_id = :%s)", params.Add(*f.AnyUserID), params.Add(*f.AnyUserID)))
params["anyuid1"] = *f.AnyUserID
params["anyuid2"] = *f.AnyUserID
} }
if f.SubscriberUserID != nil { if f.SubscriberUserID != nil {
filter := make([]string, 0) filter := make([]string, 0)
for i, v := range *f.SubscriberUserID { for _, v := range *f.SubscriberUserID {
filter = append(filter, fmt.Sprintf("(subscriber_user_id = :subscriber_uid_1_%d)", i)) filter = append(filter, fmt.Sprintf("(subscriber_user_id = :%s)", params.Add(v)))
params[fmt.Sprintf("subscriber_uid_1_%d", i)] = v
} }
sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")")
} }
if f.SubscriberUserID2 != nil { if f.SubscriberUserID2 != nil {
filter := make([]string, 0) filter := make([]string, 0)
for i, v := range *f.SubscriberUserID2 { for _, v := range *f.SubscriberUserID2 {
filter = append(filter, fmt.Sprintf("(subscriber_user_id = :subscriber_uid_2_%d)", i)) filter = append(filter, fmt.Sprintf("(subscriber_user_id = :%s)", params.Add(v)))
params[fmt.Sprintf("subscriber_uid_2_%d", i)] = v
} }
sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")")
} }
if f.ChannelOwnerUserID != nil { if f.ChannelOwnerUserID != nil {
filter := make([]string, 0) filter := make([]string, 0)
for i, v := range *f.ChannelOwnerUserID { for _, v := range *f.ChannelOwnerUserID {
filter = append(filter, fmt.Sprintf("(channel_owner_user_id = :chanowner_uid_1_%d)", i)) filter = append(filter, fmt.Sprintf("(channel_owner_user_id = :%s)", params.Add(v)))
params[fmt.Sprintf("chanowner_uid_1_%d", i)] = v
} }
sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")")
} }
if f.ChannelOwnerUserID2 != nil { if f.ChannelOwnerUserID2 != nil {
filter := make([]string, 0) filter := make([]string, 0)
for i, v := range *f.ChannelOwnerUserID2 { for _, v := range *f.ChannelOwnerUserID2 {
filter = append(filter, fmt.Sprintf("(channel_owner_user_id = :chanowner_uid_2_%d)", i)) filter = append(filter, fmt.Sprintf("(channel_owner_user_id = :%s)", params.Add(v)))
params[fmt.Sprintf("chanowner_uid_2_%d", i)] = v
} }
sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")")
} }
if f.ChannelID != nil { if f.ChannelID != nil {
filter := make([]string, 0) filter := make([]string, 0)
for i, v := range *f.ChannelID { for _, v := range *f.ChannelID {
filter = append(filter, fmt.Sprintf("(channel_id = :chanid_%d)", i)) filter = append(filter, fmt.Sprintf("(channel_id = :%s)", params.Add(v)))
params[fmt.Sprintf("chanid_%d", i)] = v
} }
sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")")
} }
@ -101,18 +94,15 @@ func (f SubscriptionFilter) SQL() (string, string, sq.PP, error) {
} }
if f.Timestamp != nil { if f.Timestamp != nil {
sqlClauses = append(sqlClauses, "(timestamp_created = :ts_equals)") sqlClauses = append(sqlClauses, fmt.Sprintf("(timestamp_created = :%s)", params.Add((*f.Timestamp).UnixMilli())))
params["ts_equals"] = (*f.Timestamp).UnixMilli()
} }
if f.TimestampAfter != nil { if f.TimestampAfter != nil {
sqlClauses = append(sqlClauses, "(timestamp_created > :ts_after)") sqlClauses = append(sqlClauses, fmt.Sprintf("(timestamp_created > :%s)", params.Add((*f.TimestampAfter).UnixMilli())))
params["ts_after"] = (*f.TimestampAfter).UnixMilli()
} }
if f.TimestampBefore != nil { if f.TimestampBefore != nil {
sqlClauses = append(sqlClauses, "(timestamp_created < :ts_before)") sqlClauses = append(sqlClauses, fmt.Sprintf("(timestamp_created < ::%s)", params.Add((*f.TimestampBefore).UnixMilli())))
params["ts_before"] = (*f.TimestampBefore).UnixMilli()
} }
sqlClause := "" sqlClause := ""

View File

@ -38,7 +38,7 @@ func TestSearchMessageFTSSimple(t *testing.T) {
Messages []msg `json:"messages"` 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.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<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" }) 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, "msgList.len", 1, len(msgList.Messages))
tt.AssertEqual(t, "msg.Title", "Notice", msgList.Messages[0].Title) 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) 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) ws, baseUrl, stop := tt.StartSimpleWebserver(t)
defer stop() defer stop()
@ -763,8 +763,8 @@ func TestListMessagesFilterChannel(t *testing.T) {
{"channel=Reminders", 6, fmt.Sprintf("/api/v2/messages?channel=%s", "Reminders")}, {"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", 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)}, {"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")}, {"search=unusual", 1, fmt.Sprintf("/api/v2/messages?search=%s", "unusual")},
{"filter=your", 6, fmt.Sprintf("/api/v2/messages?filter=%s", "your")}, {"search=your", 6, fmt.Sprintf("/api/v2/messages?search=%s", "your")},
{"prio=0", 5, fmt.Sprintf("/api/v2/messages?priority=%s", "0")}, {"prio=0", 5, fmt.Sprintf("/api/v2/messages?priority=%s", "0")},
{"prio=1", 4 + 7, fmt.Sprintf("/api/v2/messages?priority=%s", "1")}, {"prio=1", 4 + 7, fmt.Sprintf("/api/v2/messages?priority=%s", "1")},
{"prio=2", 6, fmt.Sprintf("/api/v2/messages?priority=%s", "2")}, {"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) 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)
}
}

View File

@ -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, "", "", 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", "", 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", 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, "", "", 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", 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}, {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},