Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mentions regex #72

Merged
merged 32 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d3ac000
feat(regex): add regex.ex
unenglishable Oct 17, 2023
95ead40
test(regex): add test for regex
unenglishable Oct 17, 2023
0bed58c
test(regex): refactor test names to match guidelines
unenglishable Oct 17, 2023
c53a0de
test(regex): add test string attribute
unenglishable Oct 17, 2023
a158c2b
test(regex): test regex scan for username_mention
unenglishable Oct 17, 2023
31b1717
test(regex): use Enum.each for checking usernames
unenglishable Oct 17, 2023
5fd9379
test(regex): extract usernames from test, into attribute
unenglishable Oct 17, 2023
31dae2e
test(regex): check :username_mention_curly
unenglishable Oct 17, 2023
16fd728
test(regex): mix format
unenglishable Oct 17, 2023
cffe578
test(regex): check that mention can have a trailing comma
unenglishable Oct 17, 2023
07857c1
test(regex): test mixed case mention
unenglishable Oct 17, 2023
7e84f5a
fix(regex): user_id; remove bol from pattern, use 0-9 instead of [:di…
unenglishable Oct 17, 2023
3486a18
test(regex): create test for :user_id pattern
unenglishable Oct 19, 2023
0fe5d03
test(regex): split test for mentions out
unenglishable Oct 19, 2023
4bdf2d3
test(regex): refactor module attribute names for mentions test
unenglishable Oct 19, 2023
2364a24
test(regex): mix format
unenglishable Oct 19, 2023
4659d9c
test(regex): move attributes near mentions test
unenglishable Oct 19, 2023
f226160
test(regex): remove unnecessary id_to_username_map
unenglishable Oct 19, 2023
b410f31
refactor(models/mention): use EpochtalkServer.Regex.pattern
unenglishable Oct 19, 2023
ca36d67
test(models/mention): add mention test
unenglishable Oct 19, 2023
26e4cea
test(models/mention): username_to_user_id/2
unenglishable Oct 19, 2023
d0cb639
test(models/mention): test valid mentions get generated
unenglishable Oct 19, 2023
1f31baa
test(models/mention): check that all user ids are present in mentione…
unenglishable Oct 19, 2023
6396f3e
test(models/mention): check behavior on empty user
unenglishable Oct 19, 2023
100daec
test(models/mention): refactor invalid user body
unenglishable Oct 19, 2023
3322c65
test(models/mention): test invalid thread id
unenglishable Oct 19, 2023
9dd102a
test(models/mention): test empty body
unenglishable Oct 19, 2023
c60f356
test(models/mention): test user without acl permission
unenglishable Oct 19, 2023
7effba3
test(models/mention): add note for duplicate mention
unenglishable Oct 19, 2023
e76818f
test(models/mention): test empty title
unenglishable Oct 19, 2023
75219eb
style(models/mention): mix format
unenglishable Oct 19, 2023
e53e4c7
test(models/mention): mix format
unenglishable Oct 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions lib/epochtalk_server/models/mention.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ defmodule EpochtalkServer.Models.Mention do
alias EpochtalkServerWeb.Helpers.Pagination
alias EpochtalkServerWeb.Helpers.ACL

@username_mention_regex ~r/\[code\].*\[\/code\](*SKIP)(*FAIL)|(?<=^|\s)@([a-zA-Z0-9\-_.]+)/i
@username_mention_regex_curly ~r/\[code\].*\[\/code\](*SKIP)(*FAIL)|{@([a-zA-Z0-9\-_.]+)}/i
@user_id_regex ~r/{@^[[:digit:]]+}/

@moduledoc """
`Mention` model, for performing actions relating to forum categories
"""
Expand Down Expand Up @@ -165,7 +161,7 @@ defmodule EpochtalkServer.Models.Mention do

if Map.has_key?(post, :body) do
user_ids =
Regex.scan(@user_id_regex, post.body)
Regex.scan(EpochtalkServer.Regex.pattern(:user_id), post.body)
# only need unique list of user_ids
|> Enum.uniq()
# remove "{@}" from mentioned user_id
Expand All @@ -181,7 +177,11 @@ defmodule EpochtalkServer.Models.Mention do
updated_body = String.replace(modified_post.body, "{@#{user_id}}", "@#{username}")

updated_body_html =
String.replace(modified_post.body_html, @user_id_regex, profile_link)
String.replace(
modified_post.body_html,
EpochtalkServer.Regex.pattern(:user_id),
profile_link
)

modified_post =
Map.put(modified_post, :body, updated_body) |> Map.put(:body_html, updated_body_html)
Expand Down Expand Up @@ -209,14 +209,19 @@ defmodule EpochtalkServer.Models.Mention do
post_attrs = Map.put(post_attrs, "body_original", body)

# replace "@UsErNamE" mention with "{@username}"
body = String.replace(body, @username_mention_regex, &"{#{String.downcase(&1)}}")
body =
String.replace(
body,
EpochtalkServer.Regex.pattern(:username_mention),
&"{#{String.downcase(&1)}}"
)

# update post_attrs with modified body
post_attrs = Map.put(post_attrs, "body", body)

# get list of unique usernames that were mentioned in the post body
possible_usernames =
Regex.scan(@username_mention_regex_curly, body)
Regex.scan(EpochtalkServer.Regex.pattern(:username_mention_curly), body)
# only need unique list of usernames
|> Enum.uniq()
# extract username from regex scan
Expand Down
17 changes: 17 additions & 0 deletions lib/epochtalk_server/regex.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule EpochtalkServer.Regex do
@moduledoc """
Consolidated source for hard-coded regex patterns
"""
@patterns %{
username_mention: ~r/\[code\].*\[\/code\](*SKIP)(*FAIL)|(?<=^|\s)@([a-zA-Z0-9\-_.]+)/i,
username_mention_curly: ~r/\[code\].*\[\/code\](*SKIP)(*FAIL)|{@([a-zA-Z0-9\-_.]+)}/i,
user_id: ~r/{@[0-9]+}/
}

@doc """
Given a pattern specification atom
Returns regex pattern or nil if pattern does not exist
"""
@spec pattern(pattern :: atom) :: Regex.t() | nil
def pattern(pattern), do: @patterns[pattern]
end
139 changes: 139 additions & 0 deletions test/epochtalk_server/models/mention_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
defmodule Test.EpochtalkServer.Models.Mention do
use Test.Support.ConnCase, async: true
import Test.Support.Factory
alias EpochtalkServer.Models.Mention

setup %{users: %{user: user}} do
category = insert(:category)
board = insert(:board)

build(:board_mapping,
attributes: [
[category: category, view_order: 0],
[board: board, category: category, view_order: 1]
]
)

thread = build(:thread, board: board, user: user)
{:ok, thread: thread.post.thread}
end

describe "username_to_user_id/2" do
test "given an empty user, errors", %{thread: thread} do
attrs = %{
"thread" => thread.id,
"title" => "title",
"body" => "(invalid)"
}

assert_raise FunctionClauseError,
~r/no function clause matching/,
fn ->
Mention.username_to_user_id(%{}, attrs)
end
end

test "given an invalid thread id, succeeds", %{users: %{user: user}} do
attrs = %{
"thread" => -1,
"title" => "title",
"body" => "(invalid)"
}

result = Mention.username_to_user_id(user, attrs)
assert result["body"] == attrs["body"]
assert result["body_original"] == attrs["body"]
assert result["mentioned_ids"] == []
end

test "given an empty title, succeeds", %{thread: thread, users: %{user: user}} do
attrs = %{
"thread" => thread.id,
"title" => "",
"body" => "body"
}

result = Mention.username_to_user_id(user, attrs)
assert result["body"] == attrs["body"]
assert result["body_original"] == attrs["body"]
assert result["mentioned_ids"] == []
end

test "given an empty body, succeeds", %{thread: thread, users: %{user: user}} do
attrs = %{
"thread" => thread.id,
"title" => "title",
"body" => ""
}

result = Mention.username_to_user_id(user, attrs)
assert result["body"] == attrs["body"]
assert result["body_original"] == attrs["body"]
assert result["mentioned_ids"] == []
end

@tag :banned
test "given a user without acl permission, errors", %{thread: thread} do
{:ok, user} = EpochtalkServer.Models.User.by_username("user")

attrs = %{
"thread" => thread.id,
"title" => "title",
"body" => ""
}

assert_raise EpochtalkServerWeb.CustomErrors.InvalidPermission,
~r/Forbidden, invalid permissions to perform this action/,
fn ->
Mention.username_to_user_id(user, attrs)
end
end

test "given a body with invalid mentions, does not generate mentions", %{
thread: thread,
users: %{user: user}
} do
attrs = %{
"thread" => thread.id,
"title" => "title",
"body" => """
@invalid @not_valid @no_user
this post should not @generate @ny @mentions
"""
}

result = Mention.username_to_user_id(user, attrs)
assert result["body"] == attrs["body"]
assert result["body_original"] == attrs["body"]
assert result["mentioned_ids"] == []
end

test "given a body with valid mentions, generates unique mentions", %{
thread: thread,
users: %{user: user, admin_user: admin_user, super_admin_user: super_admin_user}
} do
attrs = %{
"thread" => thread.id,
"title" => "title",
"body" => """
@#{admin_user.username} this post should mention three users @#{user.username}
@#{super_admin_user.username}, followed by invalids @not_valid @no_user
hello admin! (duplicate mention) @#{admin_user.username} @mentions
"""
}

expected_body = """
{@#{admin_user.id}} this post should mention three users {@#{user.id}}
{@#{super_admin_user.id}}, followed by invalids @not_valid @no_user
hello admin! (duplicate mention) {@#{admin_user.id}} @mentions
"""

result = Mention.username_to_user_id(user, attrs)
assert result["body_original"] == attrs["body"]
assert result["body"] == expected_body

assert Enum.sort(result["mentioned_ids"]) ==
Enum.sort([user.id, admin_user.id, super_admin_user.id])
end
end
end
126 changes: 126 additions & 0 deletions test/epochtalk_server/regex_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
defmodule Test.EpochtalkServer.Regex do
use Test.Support.ConnCase, async: true

describe "pattern/1" do
test "given valid atom, gets pattern" do
assert EpochtalkServer.Regex.pattern(:username_mention) != nil
assert EpochtalkServer.Regex.pattern(:username_mention_curly) != nil
assert EpochtalkServer.Regex.pattern(:user_id) != nil
end

test "given invalid atom, returns nil" do
assert EpochtalkServer.Regex.pattern(:bad_atom) == nil
end
end

@mentions_string """
Money printer go @brrrrr genesis block proof-of-work @blockchain Bitcoin
Improvement Proposal [email protected] Improvement Proposal segwit sats.
Hard fork to the moon hard fork soft fork key pair soft fork mining.
Soft fork [email protected] block reward mempool @hodl,
@__decentralized-deflationary_monetary.policy__full..node.

@Hodl halvening genesis block outputs, @BloCkchAIn public key
@satoshis[code]double-spend @problem[/code] @volatility
[code][code]Block height @satoshis segwit UTXO electronic cash[/code][/code]
Digital @[email protected] fork UTXO money printer go @brrrrr, price action
blocksize when @lambo! Merkle Tree hashrate?@Full node stacking sats @volatility block reward,
soft fork Merkle Tree halvening digital @signature.
"""

@mentions_usernames [
"brrrrr",
"blockchain",
"hodl",
"__decentralized-deflationary_monetary.policy__full..node.",
"Hodl",
"BloCkchAIn",
"satoshis",
"volatility",
"signature",
"brrrrr",
"lambo",
"volatility",
"signature."
]

describe "pattern/1 mentions" do
test "given :username_mention, scans string correctly" do
# scan test string for mentions
matches = Regex.scan(EpochtalkServer.Regex.pattern(:username_mention), @mentions_string)

# check usernames appear in matches
Enum.zip(matches, @mentions_usernames)
|> Enum.each(fn {match, username} ->
assert match == ["@" <> username, username]
end)
end

test "given :username_mention_curly, scans string with curly brace replacements correctly" do
# replace mentions with curly brace format
curly_test_string =
@mentions_string
|> String.replace(
EpochtalkServer.Regex.pattern(:username_mention),
&"{#{String.downcase(&1)}}"
)

# get possible username matches
matches =
Regex.scan(EpochtalkServer.Regex.pattern(:username_mention_curly), curly_test_string)

# check usernames appear in matches
Enum.zip(matches, @mentions_usernames)
|> Enum.each(fn {match, username} ->
username = String.downcase(username)
assert match == ["{@" <> username <> "}", username]
end)
end

test "given :user_id, scans string with curly brace id replacements correctly" do
# form keyword list of downcased unique usernames with index
# (provides pseudo user_id)
unique_usernames_with_index =
@mentions_usernames
|> Enum.map(&String.downcase(&1))
|> Enum.uniq()
|> Enum.with_index()

# create username to pseudo user_id map
username_to_id_map =
unique_usernames_with_index
|> Enum.into(%{})

# replace mentions with curly brace format
username_mentions_string =
@mentions_string
|> String.replace(
EpochtalkServer.Regex.pattern(:username_mention),
&"{#{String.downcase(&1)}}"
)

# replace username mentions with user_id mentions
user_id_mentions_string =
unique_usernames_with_index
|> Enum.reduce(username_mentions_string, fn {username, user_id}, acc ->
username_mention = "{@#{username}}"
user_id_mention = "{@#{user_id}}"

acc
|> String.replace(username_mention, user_id_mention)
end)

# create pseudo user_id mentions list from usernames list for checking mentions scan
user_id_mentions_list =
@mentions_usernames
|> Enum.map(fn username -> username_to_id_map[String.downcase(username)] end)

# check user_id's appear in matches()
Regex.scan(EpochtalkServer.Regex.pattern(:user_id), user_id_mentions_string)
|> Enum.zip(user_id_mentions_list)
|> Enum.each(fn {match, id} ->
assert match == ["{@" <> Integer.to_string(id) <> "}"]
end)
end
end
end
Loading