Skip to content

Commit

Permalink
Merge stable into release
Browse files Browse the repository at this point in the history
  • Loading branch information
davidmz committed Sep 19, 2024
2 parents 44fe197 + 6654dce commit b12e960
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 86 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.23.0] - Not released
### Fixed
- The 'by:' and 'date:' search operators were not working properly, they are
fixed now.

## [2.22.2] - 2024-09-02

## [2.22.1] - 2024-08-31
Expand Down
203 changes: 122 additions & 81 deletions app/support/DbAdapter/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,22 @@ const searchTrait = (superClass) =>
throw new Error(`The search query is too complex, try to simplify it`);
}

if (!viewerId && parsedQuery.some((t) => t instanceof Condition && t.condition === 'in-my')) {
if (!viewerId && parsedQuery.some((t) => isCondition(t, 'in-my'))) {
throw new Error(`Please sign in to use 'in-my:' filter`);
}

let hasPostTokens = false;
let hasCommentTokens = false;
walkWithScope(parsedQuery, (token, currentScope) => {
if (currentScope & IN_POSTS) {
hasPostTokens = true;
}

if (currentScope & IN_COMMENTS) {
hasCommentTokens = true;
}
});

// Map from username to User/Group object (or null)
const accountsMap = await this._getAccountsUsedInQuery(parsedQuery, viewerId);

Expand All @@ -55,20 +67,25 @@ const searchTrait = (superClass) =>
const postsOnlyTextQuery = getTSQuery(parsedQuery, IN_POSTS); // Search for this text only in posts
const commentsOnlyTextQuery = getTSQuery(parsedQuery, IN_COMMENTS); // Search for this text only in comments

// Authorship
const commonAuthors = namesToIds(getAuthorNames(parsedQuery, IN_ALL), accountsMap);
let postOnlyAuthors = namesToIds(getAuthorNames(parsedQuery, IN_POSTS), accountsMap);
const commentOnlyAuthors = namesToIds(getAuthorNames(parsedQuery, IN_COMMENTS), accountsMap);
// Text authorship (the 'author:'/'by:' filter)
const commonTextAuthors = namesToIds(getAuthorNames(parsedQuery, IN_ALL), accountsMap);
const postTextAuthors = namesToIds(getAuthorNames(parsedQuery, IN_POSTS), accountsMap);
const commentTextAuthors = namesToIds(getAuthorNames(parsedQuery, IN_COMMENTS), accountsMap);

// Post author filter (the 'from:' filter)
let postAuthors = namesToIds(getPostAuthorNames(parsedQuery), accountsMap);

// Date
const commonDateSQL = orJoin([
dateFiltersSQL(parsedQuery, 'p.created_at', IN_ALL),
dateFiltersSQL(parsedQuery, 'c.created_at', IN_ALL),
const postsContentDateSQL = andJoin([
contendDateSQL(parsedQuery, 'p.created_at', IN_ALL),
contendDateSQL(parsedQuery, 'p.created_at', IN_POSTS),
]);
const commentsContentDateSQL = andJoin([
contendDateSQL(parsedQuery, 'c.created_at', IN_ALL),
contendDateSQL(parsedQuery, 'c.created_at', IN_COMMENTS),
]);
const postDateSQL = dateFiltersSQL(parsedQuery, 'p.created_at', IN_POSTS);
const commentDateSQL = dateFiltersSQL(parsedQuery, 'c.created_at', IN_COMMENTS);

const dateSQL = andJoin([commonDateSQL, postDateSQL, commentDateSQL]);
const postsDateSQL = postDateFilterSQL(parsedQuery, 'p.created_at');

// Files
const fileTypesSQL = fileTypesFiltersSQL(parsedQuery, 'a');
Expand Down Expand Up @@ -109,7 +126,7 @@ const searchTrait = (superClass) =>
if (orPostsFromMe) {
postsFeedsSQL = orJoin([postsFeedsSQL, pgFormat('p.user_id=%L', viewerId)]);
} else {
postOnlyAuthors = List.intersection(postOnlyAuthors, new List([viewerId], false));
postAuthors = List.intersection(postAuthors, new List([viewerId], false));
}
}

Expand All @@ -119,21 +136,10 @@ const searchTrait = (superClass) =>
const cLikesSQL = getClikesAuthorsSQL(parsedQuery, 'cl.user_id', accountsMap);
const useCLikesTable = isNonTrivialSQL(cLikesSQL);

// Are we using the 'comments' table?
const useCommentsTable =
!!commentsOnlyTextQuery ||
!!commonTextQuery ||
!commentOnlyAuthors.isEverything() ||
!commonAuthors.isEverything() ||
useCLikesTable ||
useCommentCountersTable ||
isNonTrivialSQL(commonDateSQL) ||
isNonTrivialSQL(commentDateSQL);

// Privacy restrictions for comments
let commentsRestrictionSQL = 'true';

if (useCommentsTable) {
if (hasCommentTokens) {
const notBannedSQLFabric = await this.notBannedActionsSQLFabric(viewerId);
commentsRestrictionSQL = orJoin([
'c.id is null',
Expand Down Expand Up @@ -175,47 +181,54 @@ const searchTrait = (superClass) =>
]);

// Authors
const authorsSQL = andJoin([
commonAuthors &&
orJoin([sqlIn('p.user_id', commonAuthors), sqlIn('c.user_id', commonAuthors)]),
postOnlyAuthors && sqlIn('p.user_id', postOnlyAuthors),
commentOnlyAuthors && sqlIn('c.user_id', commentOnlyAuthors),
const postTextsAuthorsSQL = andJoin([
commonTextAuthors && sqlIn('p.user_id', commonTextAuthors),
postTextAuthors && sqlIn('p.user_id', postTextAuthors),
]);

const commentTextsAuthorsSQL = andJoin([
commonTextAuthors && sqlIn('c.user_id', commonTextAuthors),
commentTextAuthors && sqlIn('c.user_id', commentTextAuthors),
]);

const postsAuthorsFilterSQL = andJoin([postAuthors && sqlIn('p.user_id', postAuthors)]);

// Building the full query

const fromSQL = joinLines([
`from posts p`,
`join users u on p.user_id = u.uid`,
useCommentsTable && `left join comments c on c.post_id = p.uid`,
hasCommentTokens && `left join comments c on c.post_id = p.uid`,
useCLikesTable && `left join comment_likes cl on cl.comment_id = c.id`,
useFilesTable && `left join attachments a on a.post_id = p.uid`,
usePostCountersTable && `join post_counters pc on pc.post_id = p.uid`,
useCommentCountersTable && `join comment_counters cc on cc.comment_id = c.uid`,
]);

const commonWhereSQL = andJoin([
authorsSQL,
dateSQL,
postsAuthorsFilterSQL,
postsDateSQL,
postsRestrictionsSQL,
commentsRestrictionSQL,
postCountersSQL,
commentCountersSQL,
postsPrivacySQL,
]);

const [postsPartQuery, commentsPartQuery] = [postsPartTextSQL, commentsPartTextSQL].map(
(textQuery) =>
// The selecting query is the same for both UNION's members, the
// difference is only in the textQuery condition
joinLines([
`select p.uid, p.${sort}_at as date, p.id`,
fromSQL,
`where`,
andJoin([textQuery, commonWhereSQL]),
`group by p.uid, p.${sort}_at, p.id`,
`having ${andJoin([fileTypesSQL, cLikesSQL])}`,
]),
const [postsPartQuery, commentsPartQuery] = [
andJoin([postsPartTextSQL, postTextsAuthorsSQL, postsContentDateSQL]),
andJoin([commentsPartTextSQL, commentTextsAuthorsSQL, commentsContentDateSQL]),
].map((partSQL) =>
// The selecting query is almost the same for both UNION's members, the
// difference is only in the partSQL condition
joinLines([
`select p.uid, p.${sort}_at as date, p.id`,
fromSQL,
`where`,
andJoin([partSQL, commonWhereSQL]),
`group by p.uid, p.${sort}_at, p.id`,
`having ${andJoin([fileTypesSQL, cLikesSQL])}`,
]),
);

const fullSQL = joinLines([
Expand All @@ -227,7 +240,13 @@ const searchTrait = (superClass) =>
postsFeedsSQL !== 'true' &&
`with posts as materialized (select * from posts p where ${postsFeedsSQL})`,

joinLines([postsPartQuery, useCommentsTable && commentsPartQuery], 'union'),
joinLines(
[
(hasPostTokens || !hasCommentTokens) && postsPartQuery,
hasCommentTokens && commentsPartQuery,
],
'union',
),
`order by date desc, id desc limit ${+limit} offset ${+offset}`,
]);

Expand Down Expand Up @@ -387,19 +406,27 @@ function walkWithScope(tokens, action) {
continue;
}

action(token, currentScope);
action(token, token instanceof InScope ? token.scope : currentScope);
}
}

function walkInScope(tokens, scope, action) {
walkWithScope(tokens, (token, currentScope) => currentScope === scope && action(token));
}

function isCondition(token, condition) {
return token instanceof Condition && token.condition === condition;
}

function getTSQuery(tokens, targetScope) {
const result = [];

walkWithScope(tokens, (token, currentScope) => {
if (token instanceof SeqTexts && currentScope === targetScope) {
walkInScope(tokens, targetScope, (token) => {
if (token instanceof SeqTexts) {
result.push(token.toTSQuery());
}

if (token instanceof InScope && token.scope === targetScope) {
if (token instanceof InScope) {
result.push(token.text.toTSQuery());
}
});
Expand All @@ -410,29 +437,33 @@ function getTSQuery(tokens, targetScope) {
function getAuthorNames(tokens, targetScope) {
let result = List.everything();

walkWithScope(tokens, (token, currentScope) => {
if (
token instanceof Condition &&
((token.condition === 'from' && targetScope === IN_POSTS) ||
(token.condition === 'author' && targetScope === currentScope))
) {
walkInScope(tokens, targetScope, (token) => {
if (isCondition(token, 'author')) {
result = List.intersection(result, token.exclude ? List.inverse(token.args) : token.args);
}
});

return result;
}

function getPostAuthorNames(tokens) {
let result = List.everything();

for (const token of tokens) {
if (isCondition(token, 'from')) {
result = List.intersection(result, token.exclude ? List.inverse(token.args) : token.args);
}
}

return result;
}

function getClikesAuthorsSQL(tokens, field, accountsMap) {
let positive = null;
let negative = null;

walkWithScope(tokens, (token, currentScope) => {
if (
currentScope === IN_COMMENTS &&
token instanceof Condition &&
token.condition === 'cliked-by'
) {
walkInScope(tokens, IN_COMMENTS, (token) => {
if (isCondition(token, 'cliked-by')) {
if (!token.exclude) {
positive = positive ? union(positive, token.args) : uniq(token.args);
} else {
Expand All @@ -454,13 +485,24 @@ function getClikesAuthorsSQL(tokens, field, accountsMap) {
return andJoin([positiveAgg && positiveAgg, negativeAgg && sqlNot(negativeAgg)]);
}

function dateFiltersSQL(tokens, field, targetScope) {
function postDateFilterSQL(tokens, field) {
const result = [];

for (const token of tokens) {
if (isCondition(token, 'post-date')) {
result.push(intervalSQL(token, field));
}
}

return andJoin(result);
}

function contendDateSQL(tokens, field, targetScope) {
const result = [];
walkWithScope(tokens, (token, currentScope) => {
if (
token instanceof Condition &&
((token.condition === 'post-date' && targetScope === IN_POSTS) ||
(token.condition === 'date' && currentScope === targetScope))
(isCondition(token, 'post-date') && targetScope === IN_POSTS) ||
(isCondition(token, 'date') && currentScope === targetScope)
) {
result.push(intervalSQL(token, field));
}
Expand All @@ -472,17 +514,20 @@ function privacyFiltersSQL(tokens, postsTable) {
const privacyWords = ['public', 'private', 'protected'];
let positive = null;
let negative = null;
walkWithScope(tokens, (token) => {
if (token instanceof Condition && token.condition === 'is') {
const words = token.args.filter((w) => privacyWords.includes(w));

if (!token.exclude) {
positive = positive ? union(positive, words) : uniq(words);
} else {
negative = negative ? union(negative, words) : uniq(words);
}
for (const token of tokens) {
if (!isCondition(token, 'is')) {
continue;
}
});

const words = token.args.filter((w) => privacyWords.includes(w));

if (!token.exclude) {
positive = positive ? union(positive, words) : uniq(words);
} else {
negative = negative ? union(negative, words) : uniq(words);
}
}

return andJoin([
positive && orJoin(positive.map((p) => privacySQLCondition(p, postsTable))),
Expand All @@ -509,7 +554,7 @@ function countersFiltersSQL(tokens, condition, field) {
const result = [];

for (const token of tokens) {
if (token instanceof Condition && token.condition === condition) {
if (isCondition(token, condition)) {
result.push(intervalSQL(token, field));
}
}
Expand Down Expand Up @@ -546,7 +591,7 @@ function getFileTypes(tokens) {
let negative = null;

for (const token of tokens) {
if (!(token instanceof Condition) || token.condition !== 'has') {
if (!isCondition(token, 'has')) {
continue;
}

Expand Down Expand Up @@ -609,11 +654,7 @@ function fileTypesFiltersSQL(tokens, attTable) {
*/
function orPostsFromMeState(tokens) {
for (const token of tokens) {
if (
token instanceof Condition &&
token.condition === 'in-my' &&
token.args.some((a) => /discussion/.test(a))
) {
if (isCondition(token, 'in-my') && token.args.some((a) => /discussion/.test(a))) {
// ! (| posts-from:me) === & (!posts-from:me)
return !token.exclude;
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "freefeed-server",
"description": "FreeFeed is an open source FriendFeed clone (yes, it is free and open!) based on Pepyatka open-source FriendFeed clone (yes, that one is also free and open). Basically, this is a social real-time feed aggregator that allows you to share cute kittens, coordinate upcoming events, discuss any other cool stuff on the Internet or setup a private Pepyatka instance in your company.",
"homepage": "https://freefeed.net",
"version": "2.22.2",
"version": "2.23.0",
"private": true,
"scripts": {
"start": "cross-env TZ=UTC yarn babel index.js",
Expand Down
Loading

0 comments on commit b12e960

Please sign in to comment.