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

Created rich_link_controller and functionalities #227

Merged
merged 21 commits into from
May 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9b4c95d
Created rich_link_controller and functionalities
MatheusBuss May 8, 2022
75213d6
Updated rich_link to accept markdown. Added pandex
MatheusBuss May 11, 2022
18097d6
Created rich_link_controller_test. Replaced query functions
MatheusBuss May 12, 2022
c8cf9a6
Using Elixir's views. Updated tests
MatheusBuss May 13, 2022
79b4a78
Moved sanitizer from controller to view
MatheusBuss May 13, 2022
dca921a
Moved moduledoc on rich_link_controller
MatheusBuss May 13, 2022
056e6a8
Added nginx.conf and nginx instructions
MatheusBuss May 17, 2022
65cd412
Added proxy_pass to /api/rich_link
MatheusBuss May 17, 2022
3f24d53
Updated url field for users and products
MatheusBuss May 17, 2022
7899402
Updated NGINX documentation
MatheusBuss May 24, 2022
ec17754
Made requested changes to code
MatheusBuss May 24, 2022
438adf8
Fixed title appearing in quotes
MatheusBuss May 24, 2022
1933e48
Fixed underline showing as <u></u> tag
MatheusBuss May 26, 2022
d7d99ad
Removed unecessary lines on rich_link template
MatheusBuss May 26, 2022
f99fbe3
Added default image to products
MatheusBuss May 26, 2022
bd5329a
Updated rich_link_controller_test
MatheusBuss May 26, 2022
683d35c
Added whatsapp to user agents list. Updated instructions
MatheusBuss May 27, 2022
228c491
Implemented community rich link as a fallback
MatheusBuss May 30, 2022
505eebb
Updated canonical URL
MatheusBuss May 31, 2022
b45d456
Accounted for null user names
MatheusBuss May 31, 2022
8e13c49
Updated default image for products
MatheusBuss May 31, 2022
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
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ Here is our [GraphQL wiki](https://cambiatus.github.io/onboarding.md) page
- EOS Blockchain main [documentation](https://developers.eos.io/welcome/latest/overview/index) page
- Here is [our documentation](eos.md) on how we use EOS blockchain

**HTTP server**

- [NGINX](https://nginx.org/en/docs/) main documentation page

## Development Environment Setup

To build and run this application locally follow the following steps!
Expand Down Expand Up @@ -120,6 +124,47 @@ Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.

#Boom! Now you can hack away!

## NGINX setup

We use NGINX to manage our server infrastructure. You can check out how to install and run it on their [official site](https://nginx.org/)

We also use NGINX to redirect social media crawlers in order to dynamically serve rich links (or Open Graphs) in accordance to the [Open Graph Protocol](https://ogp.me/). To set this up on your NGINX follow these steps:

**Step 0**

Locate your NGINX configuration file (`nginx.conf`) and get ready to edit it

**Step 1**

Add the following lines right before any of the server blocks are declared:

```
map $http_user_agent $rich_link_prefix {
default 0;
~*(facebookexternalhit|twitterbot|telegrambot|linkedinbot|slackbot|whatsapp) /api/rich_link;
}
```

This code is responsible for detecting a crawler from different social media websites based on the user agent issued on the http request.

If a crawler is detected it saves the user agent to the `$rich_link_prefix` variable.

**Step 2**

Inside the server block referring to `server_name block staging.cambiatus.io *.staging.cambiatus.io;` under `location /` insert:

```
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
if ($rich_link_prefix != 0) {
proxy_pass http://127.0.0.1:#{backend_listening_port}$rich_link_prefix$request_uri;
}
```

Note: Replace `#{backend_listening_port}` with the port to which the backend server is setup to listen.

This code redirects the identified crawler to the correct rich link URL. Nut if a crawler was not detected the server runs normally.

## Contributing

When you are ready to make your first contribution please check out our [contribution guide](/.github/contributing.md), this will get your up to speed on where and how to start.
Expand Down
97 changes: 97 additions & 0 deletions lib/cambiatus_web/controllers/rich_link_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
defmodule CambiatusWeb.RichLinkController do
@moduledoc """
Get data and render html to be used for rich links (also known as Open Graphs).
These rich links show additional information about the website when shared on social media
and must be compliant with the [Open Grap Protocol](https://ogp.me/)
"""
lucca65 marked this conversation as resolved.
Show resolved Hide resolved

use CambiatusWeb, :controller

alias CambiatusWeb.Resolvers.{Accounts, Commune, Shop}
alias Cambiatus.Repo

def rich_link(conn, params) do
data =
with community_subdomain <- conn.host do
case Map.get(params, "page") do
["shop", id] ->
product_rich_link(id, community_subdomain)

["profile", account] ->
user_rich_link(account, community_subdomain)

_ ->
community_rich_link(community_subdomain)
MatheusBuss marked this conversation as resolved.
Show resolved Hide resolved
end
end

case data do
{:ok, data} ->
render(conn, "rich_link.html", %{data: data})
lucca65 marked this conversation as resolved.
Show resolved Hide resolved

{:error, reason} ->
send_resp(conn, 404, reason)
end
end

def product_rich_link(id, community_subdomain) do
get_image = fn product ->
with product = Repo.preload(product, :images),
[image | _] <- product.images do
image.uri
else
_ ->
"https://cambiatus-uploads.s3.amazonaws.com/cambiatus-uploads/b214c106482a46ad89f3272761d3f5b5"
end
end

case Shop.get_product(nil, %{id: id}, nil) do
{:ok, product} ->
{:ok,
%{
description: product.description,
title: product.title,
url: community_subdomain <> "/shop/#{product.id}",
image: get_image.(product),
henriquecbuss marked this conversation as resolved.
Show resolved Hide resolved
locale: nil
}}

{:error, reason} ->
{:error, reason}
end
end

def user_rich_link(account, community_subdomain) do
case Accounts.get_user(nil, %{account: account}, nil) do
{:ok, user} ->
{:ok,
%{
description: user.bio,
title: if(user.name, do: user.name, else: user.account),
url: community_subdomain <> "/profile/#{user.account}",
image: user.avatar,
locale: user.location
}}

{:error, reason} ->
{:error, reason}
end
end

def community_rich_link(community_subdomain) do
case Commune.find_community(%{}, %{subdomain: community_subdomain}, %{}) do
{:ok, community} ->
{:ok,
%{
description: community.description,
title: community.name,
url: community_subdomain,
image: community.logo,
locale: nil
}}

{:error, reason} ->
{:error, reason}
end
end
end
1 change: 1 addition & 0 deletions lib/cambiatus_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ defmodule CambiatusWeb.Router do
get("/chain/info", ChainController, :info)
post("/invite", InviteController, :invite)
get("/manifest", ManifestController, :manifest)
get("/rich_link/*page", RichLinkController, :rich_link)
lucca65 marked this conversation as resolved.
Show resolved Hide resolved

post("/paypal", PaypalController, :index)
end
Expand Down
1 change: 1 addition & 0 deletions lib/cambiatus_web/templates/layout/app.html.eex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= @inner_content %>
16 changes: 16 additions & 0 deletions lib/cambiatus_web/templates/rich_link/rich_link.html.eex
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<html lang="en" class="h-full" >
<head>
<meta charset="utf-8">

<meta name="twitter:card" content="summary">
<meta name="twitter:site" content="@_cambiatus">
<meta name="description" content="<%= md_to_txt(@data.description) %>">
<meta property="og:description" content="<%= md_to_txt(@data.description) %>">
<meta property="og:locale" content="<%= @data.locale %>">
<meta property="og:type" content="website">
<meta property="og:title" content="<%= @data.title %>">
<meta property="og:url" content="<%= @data.url %>">
<meta property="og:image" content="<%= @data.image %>">

<title><%= @data.title %></title>
</head>
17 changes: 17 additions & 0 deletions lib/cambiatus_web/views/rich_link_view.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule CambiatusWeb.RichLinkView do
use CambiatusWeb, :view

require Earmark
require HtmlSanitizeEx

def md_to_txt(markdown) do
with {:ok, string, _} <- Earmark.as_html(markdown, escape: false) do
string
|> HtmlSanitizeEx.strip_tags()
|> String.trim()
else
{:error, _} ->
""
end
end
end
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ defmodule Cambiatus.Mixfile do
{:ex_phone_number, "~> 0.2"},
{:number, "~> 1.0"},
{:earmark, "~> 1.4"},
{:html_sanitize_ex, "~> 1.4"},

# Email capabilities
{:swoosh, "~> 1.0"},
Expand Down
179 changes: 179 additions & 0 deletions test/cambiatus_web/controllers/rich_link_controller_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
defmodule CambiatusWeb.RichLinkControllerTest do
use Cambiatus.DataCase
use CambiatusWeb.ConnCase

alias Cambiatus.Repo

setup %{conn: conn} do
{:ok, conn: put_req_header(conn, "accept", "application/json")}
end

describe "RichLink" do
test "generate rich link for community",
%{conn: conn} do
# Insert community and extract data for the rich link
community =
insert(:community)
|> Repo.preload(:subdomain)

expected_data = %{
description: md_to_txt(community.description),
title: community.name,
url: community.subdomain.name,
image: community.logo,
locale: nil
}

# Submit GET request for a community rich link
conn =
%{conn | host: community.subdomain.name}
|> get("/api/rich_link")

response = html_response(conn, 200)

# Check if all the rich link fields are properly filled
Enum.each(expected_data, fn {k, v} ->
assert String.match?(response, ~r/meta property=\"og:#{k}\" content=\"#{v}/)
end)
end

test "generate rich link for user",
%{conn: conn} do
# Insert user and extract data for the rich link

user = insert(:user)

community =
insert(:community)
|> Repo.preload(:subdomain)

expected_data = %{
description: md_to_txt(user.bio),
title: user.name,
url: community.subdomain.name <> "/profile/#{user.account}",
image: user.avatar,
locale: user.location
}

# Submit GET request for a user rich link
conn =
%{conn | host: community.subdomain.name}
|> get("/api/rich_link/profile/#{user.account}")

response = html_response(conn, 200)

# Check if all the rich link fields are properly filled
Enum.each(expected_data, fn {k, v} ->
assert String.match?(response, ~r/meta property=\"og:#{k}\" content=\"#{v}/)
end)
end

test "generate rich link for user without name",
%{conn: conn} do
# Insert user and extract data for the rich link

user = insert(:user, name: nil)

community =
insert(:community)
|> Repo.preload(:subdomain)

expected_data = %{
description: md_to_txt(user.bio),
title: user.account,
url: community.subdomain.name <> "/profile/#{user.account}",
image: user.avatar,
locale: user.location
}

# Submit GET request for a user rich link
conn =
%{conn | host: community.subdomain.name}
|> get("/api/rich_link/profile/#{user.account}")

response = html_response(conn, 200)

# Check if all the rich link fields are properly filled
Enum.each(expected_data, fn {k, v} ->
assert String.match?(response, ~r/meta property=\"og:#{k}\" content=\"#{v}/)
end)
end

test "generate rich link for product with image",
%{conn: conn} do
# Insert product and extract data for the rich link
product =
insert(:product)
|> Repo.preload(:images)

[image | _] = product.images

community =
insert(:community)
|> Repo.preload(:subdomain)

expected_data = %{
description: md_to_txt(product.description),
title: product.title,
url: community.subdomain.name <> "/shop/#{product.id}",
image: image.uri,
locale: nil
}

# Submit GET request for a product rich link
conn =
%{conn | host: community.subdomain.name}
|> get("/api/rich_link/shop/#{product.id}")

response = html_response(conn, 200)

# Check if all the rich link fields are properly filled
Enum.each(expected_data, fn {k, v} ->
assert String.match?(response, ~r/meta property=\"og:#{k}\" content=\"#{v}/)
end)
end
end

test "generate rich link for product without image",
%{conn: conn} do
# Insert product without images and extract data for the rich link
product = insert(:product, images: [])

community =
insert(:community)
|> Repo.preload(:subdomain)

expected_data = %{
description: md_to_txt(product.description),
title: product.title,
url: community.subdomain.name <> "/shop/#{product.id}",
image:
"https://cambiatus-uploads.s3.amazonaws.com/cambiatus-uploads/b214c106482a46ad89f3272761d3f5b5",
locale: nil
}

# Submit GET request for a product rich link
conn =
%{conn | host: community.subdomain.name}
|> get("/api/rich_link/shop/#{product.id}")

response = html_response(conn, 200)

# Check if all the rich link fields are properly filled
Enum.each(expected_data, fn {k, v} ->
assert String.match?(response, ~r/meta property=\"og:#{k}\" content=\"#{v}/)
end)
end

defp md_to_txt(markdown) do
# Convert markdown to plain text
with {:ok, string, _} <- Earmark.as_html(markdown, escape: false) do
string
|> HtmlSanitizeEx.strip_tags()
|> String.trim()
else
{:error, _} ->
""
end
end
end