Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ecc5d7ebd
|
||
|
|
0282bc0f0a
|
||
|
|
bff8257f34
|
@@ -3,7 +3,9 @@ plugins {
|
|||||||
// START: FlutterFire Configuration
|
// START: FlutterFire Configuration
|
||||||
id 'com.google.gms.google-services'
|
id 'com.google.gms.google-services'
|
||||||
// END: FlutterFire Configuration
|
// END: FlutterFire Configuration
|
||||||
id "kotlin-android"
|
// Kotlin is provided by Flutter's built-in Kotlin support (the flutter-gradle-plugin
|
||||||
|
// auto-applies kotlin-android). KGP stays declared in settings.gradle so it remains
|
||||||
|
// on the classpath. See https://docs.flutter.dev/release/breaking-changes/migrate-to-built-in-kotlin/for-app-developers
|
||||||
id "dev.flutter.flutter-gradle-plugin"
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,10 +44,6 @@ android {
|
|||||||
targetCompatibility JavaVersion.VERSION_17
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = '17'
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main.java.srcDirs += 'src/main/kotlin'
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
}
|
}
|
||||||
@@ -75,6 +73,12 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
source '../..'
|
source '../..'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
org.gradle.jvmargs=-Xmx4G
|
org.gradle.jvmargs=-Xmx4G
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||||
|
android.builtInKotlin=false
|
||||||
|
# This newDsl flag was added automatically by Flutter migrator
|
||||||
|
android.newDsl=false
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
|||||||
|
|
||||||
Widget _buildMessageView(BuildContext context, SCNMessage message, ChannelPreview? channel, KeyTokenPreview? token, UserPreview? user) {
|
Widget _buildMessageView(BuildContext context, SCNMessage message, ChannelPreview? channel, KeyTokenPreview? token, UserPreview? user) {
|
||||||
final userAccUserID = context.select<AppAuth, String?>((v) => v.userID);
|
final userAccUserID = context.select<AppAuth, String?>((v) => v.userID);
|
||||||
|
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
|
||||||
|
|
||||||
var cfg = AppSettings();
|
var cfg = AppSettings();
|
||||||
|
|
||||||
@@ -229,7 +230,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
UI.metaCard(context: context, icon: FontAwesomeIcons.solidTimer, title: 'Timestamp', values: [message.timestamp]),
|
UI.metaCard(context: context, icon: FontAwesomeIcons.solidTimer, title: 'Timestamp', values: [dateFormat.format(DateTime.parse(message.timestamp).toLocal())]),
|
||||||
if (cfg.showExtendedAttributes)
|
if (cfg.showExtendedAttributes)
|
||||||
UI.metaCard(
|
UI.metaCard(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -319,7 +320,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
|||||||
children: [
|
children: [
|
||||||
UI.channelChip(context: context, text: _resolveChannelName(channel, message), margin: const EdgeInsets.fromLTRB(0, 0, 4, 0), fontSize: 16),
|
UI.channelChip(context: context, text: _resolveChannelName(channel, message), margin: const EdgeInsets.fromLTRB(0, 0, 4, 0), fontSize: 16),
|
||||||
Expanded(child: SizedBox()),
|
Expanded(child: SizedBox()),
|
||||||
Text(dateFormat.format(DateTime.parse(message.timestamp)), style: const TextStyle(fontSize: 14)),
|
Text(dateFormat.format(DateTime.parse(message.timestamp).toLocal()), style: const TextStyle(fontSize: 14)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
|
|||||||
@@ -119,11 +119,17 @@ func (h APIHandler) ListMessages(pctx ginext.PreContext) ginext.HTTPResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(q.Search) != 0 {
|
if len(q.Search) != 0 {
|
||||||
filter.SearchStringFTS = langext.Ptr(langext.ArrMap(q.Search, func(v string) string { return strings.TrimSpace(v) }))
|
searchTerms := langext.ArrFilter(langext.ArrMap(q.Search, func(v string) string { return strings.TrimSpace(v) }), func(v string) bool { return v != "" })
|
||||||
|
if len(searchTerms) != 0 {
|
||||||
|
filter.SearchStringFTS = langext.Ptr(searchTerms)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(q.StringSearch) != 0 {
|
if len(q.StringSearch) != 0 {
|
||||||
filter.SearchStringPlain = langext.Ptr(langext.ArrMap(q.StringSearch, func(v string) string { return strings.TrimSpace(v) }))
|
searchTerms := langext.ArrFilter(langext.ArrMap(q.StringSearch, func(v string) string { return strings.TrimSpace(v) }), func(v string) bool { return v != "" })
|
||||||
|
if len(searchTerms) != 0 {
|
||||||
|
filter.SearchStringPlain = langext.Ptr(searchTerms)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(q.Channels) != 0 {
|
if len(q.Channels) != 0 {
|
||||||
|
|||||||
@@ -4,13 +4,15 @@ import (
|
|||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"git.blackforestbytes.com/BlackForestBytes/goext/dataext"
|
"git.blackforestbytes.com/BlackForestBytes/goext/dataext"
|
||||||
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
|
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
|
||||||
"git.blackforestbytes.com/BlackForestBytes/goext/mathext"
|
"git.blackforestbytes.com/BlackForestBytes/goext/mathext"
|
||||||
"git.blackforestbytes.com/BlackForestBytes/goext/sq"
|
"git.blackforestbytes.com/BlackForestBytes/goext/sq"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type MessageFilter struct {
|
type MessageFilter struct {
|
||||||
@@ -219,7 +221,7 @@ func (f MessageFilter) SQL() (string, string, sq.PP, error) {
|
|||||||
if f.SearchStringFTS != nil {
|
if f.SearchStringFTS != nil {
|
||||||
filter := make([]string, 0)
|
filter := make([]string, 0)
|
||||||
for _, v := range *f.SearchStringFTS {
|
for _, v := range *f.SearchStringFTS {
|
||||||
filter = append(filter, fmt.Sprintf("(messages_fts match :%s)", params.Add(v)))
|
filter = append(filter, fmt.Sprintf("(messages_fts match :%s)", params.Add(sanitizeFTSMatchQuery(v))))
|
||||||
}
|
}
|
||||||
sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")")
|
sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")")
|
||||||
}
|
}
|
||||||
@@ -246,6 +248,43 @@ func (f MessageFilter) SQL() (string, string, sq.PP, error) {
|
|||||||
return sqlClause, joinClause, params, nil
|
return sqlClause, joinClause, params, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sanitizeFTSMatchQuery converts an arbitrary user-provided search string into a valid FTS5 MATCH query.
|
||||||
|
// It preserves explicit "quoted phrases" typed by the user and wraps every other whitespace-separated word
|
||||||
|
// in double-quotes, so that FTS5 special characters ( - : * ^ ( ) AND OR NOT NEAR ... ) inside plain search
|
||||||
|
// terms are treated as literal text instead of query operators.
|
||||||
|
func sanitizeFTSMatchQuery(s string) string {
|
||||||
|
out := make([]string, 0)
|
||||||
|
buf := strings.Builder{}
|
||||||
|
inQuote := false
|
||||||
|
|
||||||
|
flush := func() {
|
||||||
|
if buf.Len() > 0 {
|
||||||
|
out = append(out, `"`+strings.ReplaceAll(buf.String(), `"`, `""`)+`"`)
|
||||||
|
buf.Reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range s {
|
||||||
|
switch {
|
||||||
|
case r == '"':
|
||||||
|
// a double-quote opens or closes an explicit phrase; flush the pending word either way
|
||||||
|
flush()
|
||||||
|
inQuote = !inQuote
|
||||||
|
case !inQuote && unicode.IsSpace(r):
|
||||||
|
flush()
|
||||||
|
default:
|
||||||
|
buf.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flush() // trailing word or an unterminated phrase
|
||||||
|
|
||||||
|
if len(out) == 0 {
|
||||||
|
return `""` // valid FTS5 query that simply matches nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(out, " ")
|
||||||
|
}
|
||||||
|
|
||||||
func (f MessageFilter) Hash() string {
|
func (f MessageFilter) Hash() string {
|
||||||
bh, err := dataext.StructHash(f, dataext.StructHashOptions{HashAlgo: sha512.New()})
|
bh, err := dataext.StructHash(f, dataext.StructHashOptions{HashAlgo: sha512.New()})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -830,6 +830,11 @@ func TestListMessagesSearch(t *testing.T) {
|
|||||||
{"search=the(2)", 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=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\""))},
|
{"search=11pm", 2, fmt.Sprintf("/api/v2/messages?search=%s", url.QueryEscape("\"11:00pm\""))},
|
||||||
|
|
||||||
|
{"search=hyphen", 0, fmt.Sprintf("/api/v2/messages?search=%s", url.QueryEscape("yt-backup"))},
|
||||||
|
{"search=colon", 0, fmt.Sprintf("/api/v2/messages?search=%s", url.QueryEscape("foo:bar"))},
|
||||||
|
{"search=NOT", 0, fmt.Sprintf("/api/v2/messages?search=%s", url.QueryEscape("NOT"))},
|
||||||
|
{"search=empty", 22, fmt.Sprintf("/api/v2/messages?search=%s", url.QueryEscape(""))},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, testdata := range filterTests {
|
for _, testdata := range filterTests {
|
||||||
|
|||||||
Reference in New Issue
Block a user