7 | <% end %>
8 | Please follow this link to create your account:
9 |
10 | Create account
11 |
12 |
--------------------------------------------------------------------------------
/apps/cf/lib/mailer/templates/invitation.en.text.eex:
--------------------------------------------------------------------------------
1 | Your invitation to try CaptainFact is ready !
2 |
3 | <%= unless is_nil(@invited_by) do %>
4 | <%= user_appelation(@invited_by) %> invited you to join CaptainFact.io, a
5 | platform to collaboratively fact-check content on the Internet.
6 | <% end %>
7 |
8 | Please follow this link to create your account:
9 | <%= invitation_url(@invitation_token) %>
10 |
--------------------------------------------------------------------------------
/apps/cf/lib/mailer/templates/invitation.fr.html.eex:
--------------------------------------------------------------------------------
1 | Votre invitation pour rejoindre CaptainFact est prête !
2 |
3 | Pour créer un compte dès maintenant, utilisez votre lien d'invitation unique :
4 |
5 | Je m'inscris
6 |
7 |
8 | A très vite !
9 |
--------------------------------------------------------------------------------
/apps/cf/lib/mailer/templates/invitation.fr.text.eex:
--------------------------------------------------------------------------------
1 | Votre invitation pour rejoindre CaptainFact est prête !
2 |
3 | Pour créer un compte dès maintenant, utilisez votre lien d'invitation unique :
4 | <%= invitation_url(@invitation_token) %>
5 |
6 | A très vite !
--------------------------------------------------------------------------------
/apps/cf/lib/mailer/templates/newsletter.en.html.eex:
--------------------------------------------------------------------------------
1 | <%= raw @content %>
2 |
3 |
4 |
5 |
7 | Se désinscrire de cette newsletter
8 |
--------------------------------------------------------------------------------
/apps/cf/lib/mailer/templates/newsletter.fr.text.eex:
--------------------------------------------------------------------------------
1 | <%= @text_content %>
2 |
3 | --------------------------------------------------------------------------------
4 |
5 | Se désinscrire de cette newsletter: <%= unsubscribe_newsletter_url(@user.newsletter_subscription_token) %>
6 |
--------------------------------------------------------------------------------
/apps/cf/lib/mailer/templates/reset_password.en.html.eex:
--------------------------------------------------------------------------------
1 | You recently asked to reset your password on CF.
2 |
3 | You can do so by following this link:
4 |
5 |
6 | Reset password
7 |
8 |
9 |
10 |
11 | Please ignore this email if the request is not comming from you.
12 |
13 |
14 |
15 |
16 | Reset requested by IP: <%= @source_ip %>
17 |
--------------------------------------------------------------------------------
/apps/cf/lib/mailer/templates/reset_password.en.text.eex:
--------------------------------------------------------------------------------
1 | You recently asked to reset your password on CF.
2 |
3 | You can do so by following this link:
4 | <%= reset_password_url(@reset_password_token) %>
5 |
6 | Please ignore this email if the request is not comming from you.
7 |
8 | (Requested by IP: <%= @source_ip %>)
--------------------------------------------------------------------------------
/apps/cf/lib/mailer/templates/reset_password.fr.html.eex:
--------------------------------------------------------------------------------
1 | Vous avez demandé la réinitialisation de votre mot de passe sur CF.
2 | Vous pouvez procéder au changement de mot de passe en suivant ce lien :
3 |
4 |
5 | Changer mon mot de passe
6 |
7 |
8 |
9 |
10 | Merci d'ignorer cet email si la requête ne vient pas de vous.
11 |
12 |
13 |
14 |
15 | Origine de la demande : <%= @source_ip %>
16 |
--------------------------------------------------------------------------------
/apps/cf/lib/mailer/templates/reset_password.fr.text.eex:
--------------------------------------------------------------------------------
1 | Vous avez demandé la réinitialisation de votre mot de passe sur CF.
2 |
3 | Vous pouvez procéder au changement de mot de passe en suivant ce lien :
4 | <%= reset_password_url(@reset_password_token) %>
5 |
6 | Merci d'ignorer cet email si la requête ne vient pas de vous.
7 |
8 | (Origine de la demande : <%= @source_ip %>)
--------------------------------------------------------------------------------
/apps/cf/lib/mailer/templates/welcome.en.html.eex:
--------------------------------------------------------------------------------
1 | Welcome to CaptainFact.io!
2 |
3 | To confirm your email and gain a bonus of
4 | +<%= @confirm_email_reputation %> reputation, click on the following link:
5 |
6 | Confirm my email
7 |
8 |
9 | You can learn more about how the system works by
10 | checking the help pages.
11 |
12 | Feel free to contact us
13 | at contact@captainfact.io.
14 |
15 | See you soon on CaptainFact !
16 |
--------------------------------------------------------------------------------
/apps/cf/lib/mailer/templates/welcome.en.text.eex:
--------------------------------------------------------------------------------
1 | Welcome to CaptainFact.io!
2 |
3 | To confirm your email and gain a bonus of
4 | +<%= @confirm_email_reputation %> reputation, click on the following link:
5 |
6 | <%= confirm_email_url(@user.email_confirmation_token) %>
7 |
8 | You can learn more about how the system works and the whys of CaptainFact by
9 | checking the help pages at <%= help_url() %>
10 |
11 | Feel free to contact us at contact@captainfact.io
12 |
13 | See you soon on CaptainFact !
--------------------------------------------------------------------------------
/apps/cf/lib/mailer/templates/welcome.fr.html.eex:
--------------------------------------------------------------------------------
1 | Bienvenue sur CaptainFact !
2 |
3 | Pour confirmer votre adresse email et obtenir un bonus de
4 | +<%= @confirm_email_reputation %> de réputation, cliquez sur le lien suivant :
5 |
6 | Confirmer mon adresse email
7 |
8 |
9 | Vous pouvez en apprendre plus sur le fonctionnement du système en
10 | allant voir les pages d'aide.
11 |
12 | Vous pouvez également nous contacter
13 | sur contact@captainfact.io.
14 |
15 | Bon fact-checking !
16 |
--------------------------------------------------------------------------------
/apps/cf/lib/mailer/templates/welcome.fr.text.eex:
--------------------------------------------------------------------------------
1 | Bienvenue sur CaptainFact !
2 |
3 | Pour confirmer votre adresse email et obtenir un bonus de
4 | +<%= @confirm_email_reputation %> de réputation, cliquez sur le lien suivant :
5 |
6 | <%= confirm_email_url(@user.email_confirmation_token) %>
7 |
8 | Vous pouvez en apprendre plus sur le fonctionnement du système en
9 | allant voir les pages d'aide : <%= help_url() %>
10 |
11 | Vous pouvez également nous contacter contact@captainfact.io
12 |
13 | Bon fact-checking !
14 |
--------------------------------------------------------------------------------
/apps/cf/lib/mailer/view.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Mailer.View do
2 | use Phoenix.View, root: "lib/mailer/templates", namespace: CF.Mailer
3 | use Phoenix.HTML
4 |
5 | import CF.Gettext
6 | import CF.Utils.FrontendRouter
7 |
8 | def user_appelation(user) do
9 | DB.Schema.User.user_appelation(user)
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/apps/cf/lib/moderation/moderation_entry.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Moderation.ModerationEntry do
2 | defstruct action: nil, flags: []
3 | end
4 |
--------------------------------------------------------------------------------
/apps/cf/lib/sources/sources.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Sources do
2 | @moduledoc """
3 | Functions to manage sources and their metadata.
4 | """
5 |
6 | alias DB.Repo
7 | alias DB.Schema.Source
8 |
9 | alias CF.Sources.Fetcher
10 |
11 | @doc """
12 | Get a source from `DB` from its URL. Returns nil if no source exist for
13 | this URL.
14 | """
15 | @spec get_by_url(binary()) :: Source.t() | nil
16 | def get_by_url(url) do
17 | Repo.get_by(Source, url: url)
18 | end
19 |
20 | @doc """
21 | Fetch a source metadata using `CF.Sources.Fetcher`, update source with it then
22 | call `callback` (if any) with the update source.
23 | """
24 | def update_source_metadata(base_source = %Source{}, callback \\ nil) do
25 | Fetcher.fetch_source_metadata(base_source.url, fn
26 | metadata when metadata == %{} ->
27 | nil
28 |
29 | metadata ->
30 | updated_source = Repo.update!(Source.changeset_fetched(base_source, metadata))
31 | if !is_nil(callback), do: callback.(updated_source)
32 | end)
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/apps/cf/lib/statements/statements.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Statements do
2 | @moduledoc """
3 | Functions to manipulate statements
4 | """
5 |
6 | alias Ecto.Multi
7 | alias Kaur.Result
8 |
9 | alias DB.Schema.Statement
10 | alias DB.Repo
11 |
12 | alias CF.Accounts.UserPermissions
13 | import CF.Actions.ActionCreator, only: [action_update: 2]
14 |
15 | @doc """
16 | Update given statement. Will raise if user doesn't have
17 | permission to do that.
18 | """
19 | def update!(user_id, statement = %Statement{is_removed: false}, changes) do
20 | UserPermissions.check!(user_id, :update, :statement)
21 | changeset = Statement.changeset(statement, changes)
22 |
23 | if changeset.changes == %{} do
24 | Result.ok(statement)
25 | else
26 | Multi.new()
27 | |> Multi.update(:statement, changeset)
28 | |> Multi.insert(:action_update, action_update(user_id, changeset))
29 | |> Repo.transaction()
30 | |> case do
31 | {:ok, %{statement: updated_statement}} ->
32 | Result.ok(updated_statement)
33 |
34 | {:error, _operation, reason, _changes} ->
35 | Result.error(reason)
36 | end
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/apps/cf/lib/utils/utils.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Utils do
2 | @moduledoc """
3 | Helpers / utils functions
4 | """
5 |
6 | @doc """
7 | Load a YAML map from given file and recursively convert all keys to atoms.
8 | (!) This function uses `String.to_existing_atom/1` so atom must already exist
9 | """
10 | def load_yaml_config(filename) do
11 | filename
12 | |> YamlElixir.read_all_from_file!()
13 | |> List.first()
14 | |> map_string_keys_to_atom_keys()
15 | end
16 |
17 | @doc """
18 | Transform all map binary indexes to atoms. Use this function carefuly,
19 | generating too much atoms (for example when accepting user's input) can
20 | result in terrible performances issues.
21 |
22 | ## Examples
23 |
24 | iex> CF.Utils.map_string_keys_to_atom_keys(%{"test" => %{"ok" => 42}})
25 | %{test: %{ok: 42}}
26 |
27 | """
28 | def map_string_keys_to_atom_keys(map) when is_map(map) do
29 | Enum.reduce(map, %{}, fn {key, value}, result ->
30 | atom_key = convert_key_to_atom(key)
31 | converted_value = map_string_keys_to_atom_keys(value)
32 | Map.put(result, atom_key, converted_value)
33 | end)
34 | end
35 |
36 | def map_string_keys_to_atom_keys(value),
37 | do: value
38 |
39 | def truncate(text, max_length, replacement \\ "…") do
40 | if String.length(text) > max_length do
41 | String.slice(text, 0, max_length - String.length(replacement)) <> replacement
42 | else
43 | text
44 | end
45 | end
46 |
47 | # Convert key to atom if key is in binary format
48 |
49 | defp convert_key_to_atom(key) when is_binary(key),
50 | do: String.to_atom(key)
51 |
52 | defp convert_key_to_atom(key) when is_atom(key),
53 | do: key
54 | end
55 |
--------------------------------------------------------------------------------
/apps/cf/lib/video_debate/history.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.VideoDebate.History do
2 | import Ecto.Query
3 |
4 | alias DB.Repo
5 | alias DB.Schema.UserAction
6 |
7 | @allowed_entities [:statement, :speaker, :video]
8 |
9 | def video_history(video_id) do
10 | UserAction
11 | |> preload(:user)
12 | |> where([a], a.video_id == ^video_id)
13 | |> where([a], a.entity in ^@allowed_entities)
14 | |> Repo.all()
15 | end
16 |
17 | def statement_history(statement_id) do
18 | UserAction
19 | |> preload(:user)
20 | |> where([a], a.entity == ^:statement)
21 | |> where([a], a.statement_id == ^statement_id)
22 | |> Repo.all()
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/apps/cf/lib/videos/captions_fetcher.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Videos.CaptionsFetcher do
2 | @moduledoc """
3 | Fetch captions for videos.
4 | """
5 |
6 | @callback fetch(DB.Schema.Video.t()) ::
7 | {:ok, %{raw: String.t(), parsed: String.t(), format: String.t()}} | {:error, term()}
8 | end
9 |
--------------------------------------------------------------------------------
/apps/cf/lib/videos/captions_fetcher_test.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Videos.CaptionsFetcherTest do
2 | @moduledoc """
3 | A mock for faking captions fetching requests.
4 | """
5 |
6 | @behaviour CF.Videos.CaptionsFetcher
7 |
8 | @impl true
9 | def fetch(_video) do
10 | captions = %{
11 | raw: "__TEST-CONTENT__",
12 | format: "custom",
13 | parsed: [
14 | %{
15 | "text" => "__TEST-CONTENT__",
16 | "start" => 0.0,
17 | "duration" => 1.0
18 | }
19 | ]
20 | }
21 |
22 | {:ok, captions}
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/apps/cf/lib/videos/captions_srv1_parser.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Videos.CaptionsSrv1Parser do
2 | @moduledoc """
3 | A captions parser for the srv1 format.
4 | """
5 |
6 | require Logger
7 | import SweetXml
8 |
9 | def parse_file(content) do
10 | content
11 | |> SweetXml.xpath(
12 | ~x"//transcript/text"l,
13 | text: ~x"./text()"s |> transform_by(&clean_text/1),
14 | start: ~x"./@start"s |> transform_by(&parse_float/1),
15 | duration: ~x"./@dur"os |> transform_by(&parse_float/1)
16 | )
17 | |> Enum.filter(fn %{text: text, start: start} ->
18 | # Filter out text in brackets, like "[Music]"
19 | start != nil and text != nil and text != "" and
20 | String.match?(text, ~r/^\[.*\]$/) == false
21 | end)
22 | end
23 |
24 | defp clean_text(text) do
25 | text
26 | |> String.replace("&", "&")
27 | |> HtmlEntities.decode()
28 | |> String.trim()
29 | end
30 |
31 | defp parse_float(val) do
32 | case Float.parse(val) do
33 | {num, _} -> num
34 | _ -> nil
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/apps/cf/lib/videos/metadata_fetcher.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Videos.MetadataFetcher do
2 | @moduledoc """
3 | Fetch metadata for video.
4 | """
5 |
6 | @type video_metadata :: %{
7 | title: String.t(),
8 | language: String.t(),
9 | url: String.t()
10 | }
11 |
12 | @doc """
13 | Takes an URL, fetch the metadata and return them
14 | """
15 | @callback fetch_video_metadata(String.t()) :: {:ok, video_metadata} | {:error, binary()}
16 | end
17 |
--------------------------------------------------------------------------------
/apps/cf/lib/videos/metadata_fetcher_opengraph.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Videos.MetadataFetcher.Opengraph do
2 | @moduledoc """
3 | Methods to fetch metadata (title, language) from videos
4 | """
5 |
6 | @behaviour CF.Videos.MetadataFetcher
7 |
8 | @doc """
9 | Fetch metadata from video using OpenGraph tags.
10 | """
11 | def fetch_video_metadata(url) do
12 | case HTTPoison.get(url) do
13 | {:ok, %HTTPoison.Response{body: body}} ->
14 | meta = Floki.attribute(body, "meta[property='og:title']", "content")
15 |
16 | case meta do
17 | [] -> {:error, "Page does not contains an OpenGraph title attribute"}
18 | [title] -> {:ok, %{title: HtmlEntities.decode(title), url: url}}
19 | end
20 |
21 | {_, _} ->
22 | {:error, "Remote URL didn't respond correctly"}
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/apps/cf/lib/videos/metadata_fetcher_test.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Videos.MetadataFetcher.Test do
2 | @moduledoc """
3 | Methods to fetch metadata (title, language) from videos
4 | """
5 |
6 | @behaviour CF.Videos.MetadataFetcher
7 |
8 | @doc """
9 | Fetch metadata from video using OpenGraph tags.
10 | """
11 | def fetch_video_metadata(url) do
12 | {:ok,
13 | %{
14 | title: "__TEST-TITLE__",
15 | url: url
16 | }}
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/apps/cf/priv/gettext/mail.pot:
--------------------------------------------------------------------------------
1 | ## This file is a PO Template file.
2 | ##
3 | ## `msgid`s here are often extracted from source code.
4 | ## Add new translations manually only if they're dynamic
5 | ## translations that can't be statically extracted.
6 | ##
7 | ## Run `mix gettext.extract` to bring this file up to
8 | ## date. Leave `msgstr`s empty as changing them here as no
9 | ## effect: edit them in PO (`.po`) files instead.
10 | msgid ""
11 | msgstr ""
12 |
13 | #: lib/mailer/email.ex:93 lib/mailer/email.ex:93
14 | msgid "%{name} invited you to try CaptainFact.io!"
15 | msgstr ""
16 |
17 | #: lib/mailer/email.ex:60 lib/mailer/email.ex:60
18 | msgid "CaptainFact.io - Reset your password"
19 | msgstr ""
20 |
21 | #: lib/mailer/email.ex:73 lib/mailer/email.ex:73
22 | msgid "About your recent loss of reputation on CaptainFact"
23 | msgstr ""
24 |
25 | #: lib/mailer/templates/_layout.html.eex:189
26 | #: lib/mailer/templates/_layout.html.eex:189
27 | #: lib/mailer/templates/_layout.text.eex:6
28 | #: lib/mailer/templates/_layout.text.eex:6
29 | msgid "CaptainFact is a free and open source project."
30 | msgstr ""
31 |
32 | #: lib/mailer/templates/_layout.html.eex:190
33 | #: lib/mailer/templates/_layout.html.eex:190
34 | #: lib/mailer/templates/_layout.text.eex:7
35 | #: lib/mailer/templates/_layout.text.eex:7
36 | msgid "If you like it, please share the word!"
37 | msgstr ""
38 |
39 | #: lib/mailer/email.ex:32 lib/mailer/email.ex:32
40 | msgid "Confirm your CaptainFact account"
41 | msgstr ""
42 |
43 | #: lib/mailer/email.ex:89 lib/mailer/email.ex:89
44 | msgid "Your invitation to try CaptainFact.io is ready!"
45 | msgstr ""
46 |
--------------------------------------------------------------------------------
/apps/cf/priv/reputation_changes.yaml:
--------------------------------------------------------------------------------
1 | # This file describe reputation changes for given actions and optionals entity
2 | # A change is defined as a tuple like [self_change, other_user_change]
3 |
4 | # ---- Votes ----
5 |
6 | # Vote UP. Please ensure vote_up and revert_vote_up values match !
7 |
8 | vote_up:
9 | comment: [0, 2]
10 | fact: [0, 3]
11 |
12 | revert_vote_up:
13 | comment: [0, -2]
14 | fact: [0, -3]
15 |
16 | # Vote DOWN. Please ensure vote_down and revert_vote_down values match !
17 |
18 | vote_down:
19 | comment: [-1, -2]
20 | fact: [-1, -3]
21 |
22 | revert_vote_down:
23 | comment: [+1 , +2]
24 | fact: [+1 , +3]
25 |
26 | # ---- Moderation ----
27 |
28 | # Target user got its comment banned
29 |
30 | action_banned_bad_language: [0, -25]
31 | action_banned_spam: [0, -30]
32 | action_banned_irrelevant: [0, -10]
33 | action_banned_not_constructive: [0, -5]
34 |
35 | # Source user (who made the flag) has made a good or bad flag
36 |
37 | abused_flag: [0, -5]
38 | confirmed_flag: [0, +3]
39 |
40 | # ---- Misc ----
41 |
42 | email_confirmed: [0, +15]
43 |
--------------------------------------------------------------------------------
/apps/cf/test/actions/flagger_test.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.Moderation.FlaggerTest do
2 | use CF.DataCase
3 |
4 | alias DB.Schema.User
5 | alias DB.Schema.Comment
6 | alias DB.Schema.Flag
7 |
8 | alias CF.Moderation.Flagger
9 | alias CF.Jobs.{Flags, Reputation}
10 | alias CF.Moderation
11 |
12 | @nb_flags_to_report Moderation.nb_flags_to_report(:create, :comment)
13 |
14 | setup do
15 | Repo.delete_all(Flag)
16 | Repo.delete_all(User)
17 | target_user = insert(:user, %{reputation: 10_000})
18 | comment = insert(:comment, %{user: target_user}) |> with_action
19 | source_users = insert_list(@nb_flags_to_report, :user, %{reputation: 10_000})
20 | {:ok, [source_users: source_users, target_user: target_user, comment: comment]}
21 | end
22 |
23 | test "flags get inserted in DB", context do
24 | source = List.first(context[:source_users])
25 | comment = context[:comment]
26 |
27 | Flagger.flag!(source.id, comment.statement.video_id, comment, 1)
28 | Reputation.update()
29 | Flags.update()
30 | assert Flagger.get_nb_flags(comment) == 1
31 | end
32 |
33 | test "comment reported after x flags", context do
34 | comment = context[:comment]
35 |
36 | for source <- context[:source_users],
37 | do: Flagger.flag!(source.id, comment.statement.video_id, comment, 1)
38 |
39 | Reputation.update()
40 | Flags.update()
41 | assert Repo.get(Comment, comment.id).is_reported == true
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/apps/cf/test/actions/reputation_change_config_loader_test.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.Actions.ReputationChangeConfigLoaderTest do
2 | use CF.DataCase
3 | doctest CF.Actions.ReputationChangeConfigLoader
4 | end
5 |
--------------------------------------------------------------------------------
/apps/cf/test/algolia/speakers_index_test.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.Algolia.SpeakersIndexTest do
2 | use CF.DataCase
3 | doctest CF.Algolia.SpeakersIndex
4 | end
5 |
--------------------------------------------------------------------------------
/apps/cf/test/algolia/statements_index_test.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.Algolia.StatementsIndexTest do
2 | use CF.DataCase
3 | doctest CF.Algolia.StatementsIndex
4 | end
5 |
--------------------------------------------------------------------------------
/apps/cf/test/algolia/videos_index_test.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.Algolia.VideosIndexTest do
2 | use CF.DataCase
3 | doctest CF.Algolia.VideosIndex
4 | end
5 |
--------------------------------------------------------------------------------
/apps/cf/test/authenticator/authenticator_test.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.AuthenticatorTest do
2 | use CF.DataCase
3 | use ExUnitProperties
4 |
5 | alias CF.Authenticator
6 |
7 | describe "Identity" do
8 | test "can login with email" do
9 | password = "password458"
10 | user = insert_user_with_custom_password(password)
11 | authenticated_user = Authenticator.get_user_for_email_or_name_password(user.email, password)
12 |
13 | assert user.id == authenticated_user.id
14 | end
15 |
16 | test "can login with name" do
17 | password = "password458"
18 | user = insert_user_with_custom_password(password)
19 |
20 | authenticated_user =
21 | Authenticator.get_user_for_email_or_name_password(user.username, password)
22 |
23 | assert user.id == authenticated_user.id
24 | end
25 |
26 | property "password must be correct" do
27 | password =
28 | "IfPropertyTestingFailsWithThisString,itIsNotABugButAVeryVeryRareCase,iMeanAlmostImpossible!!!"
29 |
30 | user = insert_user_with_custom_password(password)
31 |
32 | check all(password <- binary(), max_runs: 3) do
33 | assert is_nil(Authenticator.get_user_for_email_or_name_password(user.email, password))
34 | end
35 | end
36 | end
37 |
38 | defp insert_user_with_custom_password(password) do
39 | insert(:user, %{encrypted_password: Bcrypt.hash_pwd_salt(password)})
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/apps/cf/test/authenticator/oauth/facebook_test.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.Authenticator.OAuth.FacebookTest do
2 | import DB.Factory
3 | use CF.DataCase
4 | import Mock
5 | alias CF.Authenticator.OAuth.Facebook
6 |
7 | doctest Facebook
8 |
9 | # TODO add arity
10 | describe "revoke_permissions/1" do
11 | test "sends an HTTP DELETE request to facebook" do
12 | user =
13 | :user
14 | |> build
15 | |> with_fb_user_id
16 | |> insert
17 |
18 | facebook_user_perms_url = "/#{user.fb_user_id}/permissions"
19 |
20 | # defining Mock for OAuth2 Client module
21 | with_mock OAuth2.Client,
22 | # Unmocked functions will be pass to original module
23 | [:passthrough],
24 | # mock delete function
25 | delete: fn _client, url when facebook_user_perms_url == url ->
26 | {:ok, %OAuth2.Response{status_code: 200, body: %{data: "success"}}}
27 | end do
28 | Facebook.revoke_permissions(user)
29 |
30 | # Check that the call was made as we expected
31 | assert called(OAuth2.Client.delete(:_, facebook_user_perms_url))
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/apps/cf/test/mailer/fomatter_test.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.Mailer.FormatterTest do
2 | use CF.DataCase
3 |
4 | test "format email address" do
5 | user = DB.Factory.build(:user)
6 | {appelation, email} = Bamboo.Formatter.format_email_address(user, [])
7 | assert email == user.email
8 | assert appelation =~ user.username
9 | assert appelation =~ user.name
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/apps/cf/test/notifications/notification_builder_test.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.Notifications.NotificationBuilderTest do
2 | use CF.DataCase
3 | alias DB.Schema.Subscription
4 | alias DB.Schema.UserAction
5 | alias CF.Notifications.NotificationBuilder
6 | doctest CF.Notifications.NotificationBuilder
7 | end
8 |
--------------------------------------------------------------------------------
/apps/cf/test/notifications/notifications_test.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.NotificationsTest do
2 | use CF.DataCase
3 |
4 | alias CF.Notifications
5 |
6 | describe "all" do
7 | setup do
8 | user = insert(:user)
9 | notifs = insert_list(5, :notification, user: user)
10 |
11 | sorted_notifs_ids =
12 | notifs |> Enum.sort_by(& &1.inserted_at, &>=/2) |> Enum.map(&Map.get(&1, :id))
13 |
14 | [user: user, notifs: notifs, sorted_notifs_ids: sorted_notifs_ids]
15 | end
16 |
17 | test "sorts notifications", %{user: user, sorted_notifs_ids: sorted_notifs_ids} do
18 | returned_ids = Enum.map(Notifications.all(user, 1, 5), & &1.id)
19 | assert returned_ids == sorted_notifs_ids
20 | end
21 | end
22 |
23 | describe "create!" do
24 | end
25 |
26 | describe "mark_as_seen/1" do
27 | test "mark the notification as seen" do
28 | notification = insert(:notification, seen_at: nil)
29 | {:ok, updated} = Notifications.mark_as_seen(notification, true)
30 | refute is_nil(updated.seen_at)
31 | end
32 |
33 | test "mark the notification as unseen" do
34 | notification = insert(:notification, seen_at: DateTime.utc_now())
35 | {:ok, updated} = Notifications.mark_as_seen(notification, false)
36 | assert is_nil(updated.seen_at)
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/apps/cf/test/sources/sources_test.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.SourcesTest do
2 | end
3 |
--------------------------------------------------------------------------------
/apps/cf/test/support/test_utils.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.TestUtils do
2 | import DB.Factory
3 | import Ecto.Query
4 | import ExUnit.Assertions
5 |
6 | alias DB.Repo
7 | alias DB.Schema.Comment
8 | alias DB.Schema.UserAction
9 |
10 | def flag_comments(comments, nb_flags, reason \\ 1) do
11 | users = insert_list(nb_flags, :user, %{reputation: 1000})
12 |
13 | flags =
14 | Enum.map(comments, fn comment ->
15 | full_comment = Repo.preload(comment, :statement)
16 |
17 | Enum.map(users, fn user ->
18 | CF.Moderation.Flagger.flag!(
19 | user.id,
20 | full_comment.statement.video_id,
21 | full_comment,
22 | reason
23 | )
24 | end)
25 | end)
26 |
27 | List.flatten(flags)
28 | end
29 |
30 | def assert_deleted(%Comment{id: id}, check_actions \\ true) do
31 | {comment, actions} = get_comment_and_actions(id)
32 | assert is_nil(comment)
33 |
34 | if check_actions do
35 | assert Enum.count(actions) == 0
36 | end
37 | end
38 |
39 | def assert_not_deleted(%Comment{id: id}) do
40 | {comment, _} = get_comment_and_actions(id)
41 | assert comment != nil
42 |
43 | assert Repo.get_by(
44 | UserAction,
45 | entity: :comment,
46 | type: :delete,
47 | comment_id: id
48 | ) == nil
49 | end
50 |
51 | defp get_comment_and_actions(id) do
52 | actions =
53 | UserAction
54 | |> where([a], a.entity == ^:comment and a.comment_id == ^id)
55 | |> Repo.all()
56 |
57 | {Repo.get(Comment, id), actions}
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/apps/cf/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | # Start everything
2 |
3 | Ecto.Adapters.SQL.Sandbox.mode(DB.Repo, {:shared, self()})
4 | {:ok, _} = Application.ensure_all_started(:bypass)
5 |
6 | ExUnit.start()
7 |
--------------------------------------------------------------------------------
/apps/cf/test/utils/cf_utils_test.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.UtilsTest do
2 | use ExUnit.Case
3 | doctest CF.Utils
4 |
5 | describe "map_string_keys_to_atom_keys" do
6 | test "convert map recursively" do
7 | base_map = %{
8 | "test" => %{
9 | "hello" => :ok
10 | },
11 | "test2" => :ok
12 | }
13 |
14 | expected_map = %{
15 | test: %{
16 | hello: :ok
17 | },
18 | test2: :ok
19 | }
20 |
21 | assert CF.Utils.map_string_keys_to_atom_keys(base_map) == expected_map
22 | end
23 |
24 | test "works with empty map" do
25 | assert CF.Utils.map_string_keys_to_atom_keys(%{}) == %{}
26 | end
27 |
28 | test "doesn't crash if binary and atom keys are mixed" do
29 | base_map = %{
30 | "test" => %{
31 | hello: 42
32 | },
33 | test_again: :ok
34 | }
35 |
36 | expected_map = %{
37 | test: %{
38 | hello: 42
39 | },
40 | test_again: :ok
41 | }
42 |
43 | assert CF.Utils.map_string_keys_to_atom_keys(base_map) == expected_map
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/apps/cf/test/utils/frontend_router_test.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.Utils.FrontendRouterTest do
2 | use ExUnit.Case
3 | doctest CF.Utils.FrontendRouter
4 | end
5 |
--------------------------------------------------------------------------------
/apps/cf_atom_feed/.gitignore:
--------------------------------------------------------------------------------
1 | # App artifacts
2 | /_build
3 | /db
4 | /deps
5 | /*.ez
6 |
7 | # Generated on crash by the VM
8 | erl_crash.dump
9 |
10 | # Files matching config/*.secret.exs pattern contain sensitive
11 | # data and you should not commit them into version control.
12 | #
13 | # Alternatively, you may comment the line below and commit the
14 | # secrets files as long as you replace their contents by environment
15 | # variables.
16 | /config/*.secret.exs
17 |
--------------------------------------------------------------------------------
/apps/cf_atom_feed/README.md:
--------------------------------------------------------------------------------
1 | # [CaptainFact App] CF Atom Feed
2 |
3 | ## Secrets
4 |
5 | Following secrets must be configured in production:
6 |
7 | - db_hostname
8 | - db_username
9 | - db_password
10 | - db_name
11 | - frontend_url
12 |
--------------------------------------------------------------------------------
/apps/cf_atom_feed/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | #
4 | # This configuration file is loaded before any dependency and
5 | # is restricted to this project.
6 | use Mix.Config
7 |
8 | # General application configuration
9 | config :cf_atom_feed,
10 | namespace: CF.AtomFeed,
11 | ecto_repos: [DB.Repo]
12 |
13 | # Configures Elixir's Logger
14 | config :logger, :console,
15 | format: "$time $metadata[$level] $message\n",
16 | metadata: [:request_id]
17 |
18 | # Configure Postgres pool size
19 | config :db, DB.Repo, pool_size: 1
20 |
21 | # Import environment specific config. This must remain at the bottom
22 | # of this file so it overrides the configuration defined above.
23 | import_config "#{Mix.env()}.exs"
24 |
--------------------------------------------------------------------------------
/apps/cf_atom_feed/config/dev.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # Do not include metadata nor timestamps in development logs
4 | config :logger, :console, format: "[$level] $message\n"
5 |
6 | config :cf_atom_feed,
7 | CF.AtomFeed.Router,
8 | cowboy: [port: 4004]
9 |
--------------------------------------------------------------------------------
/apps/cf_atom_feed/config/prod.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # Do not print debug messages in production
4 | config :logger, level: :info
5 |
--------------------------------------------------------------------------------
/apps/cf_atom_feed/config/test.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # Print only warnings and errors during test
4 | config :logger, level: :warn
5 |
6 | # Use a different port in test to avoid conflicting with dev server
7 | config :cf_atom_feed, CF.AtomFeed.Router, cowboy: [port: 10004]
8 |
--------------------------------------------------------------------------------
/apps/cf_atom_feed/lib/application.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.AtomFeed.Application do
2 | use Application
3 |
4 | # See https://hexdocs.pm/elixir/Application.html
5 | # for more information on OTP Applications
6 | def start(_type, _args) do
7 | import Supervisor.Spec
8 |
9 | children = []
10 | config = Application.get_env(:cf_atom_feed, CF.AtomFeed.Router)
11 |
12 | if config[:cowboy] do
13 | children = [supervisor(CF.AtomFeed.Router, []) | children]
14 | end
15 |
16 | # See https://hexdocs.pm/elixir/Supervisor.html
17 | # for other strategies and supported options
18 | opts = [strategy: :one_for_one, name: CF.AtomFeed.Supervisor]
19 | Supervisor.start_link(children, opts)
20 | end
21 |
22 | def config_change(_changed, _new, _removed) do
23 | :ok
24 | end
25 |
26 | def version() do
27 | case :application.get_key(:cf_atom_feed, :vsn) do
28 | {:ok, version} -> to_string(version)
29 | _ -> "unknown"
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/apps/cf_atom_feed/lib/common.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.AtomFeed.Common do
2 | @moduledoc """
3 | Common ATOM feed functions
4 | """
5 |
6 | @doc """
7 | Default feed author
8 | """
9 | def feed_author(feed),
10 | do: Atomex.Feed.author(feed, "CaptainFact", email: "atom-feed@captainfact.io")
11 |
12 | @doc """
13 | Feed base URL
14 | """
15 | def feed_base_url(),
16 | do: "https://feed.captainfact.io/"
17 | end
18 |
--------------------------------------------------------------------------------
/apps/cf_atom_feed/lib/router.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.AtomFeed.Router do
2 | use Plug.Router
3 | require Logger
4 |
5 | plug(Plug.Head)
6 | plug(:match)
7 | plug(:dispatch)
8 |
9 | def start_link do
10 | config = Application.get_env(:cf_atom_feed, CF.AtomFeed.Router)
11 | Logger.info("Running CF.AtomFeed.Router with cowboy on port #{config[:cowboy][:port]}")
12 | Plug.Cowboy.http(CF.AtomFeed.Router, [], config[:cowboy])
13 | end
14 |
15 | get "/" do
16 | conn
17 | |> put_resp_content_type("application/json")
18 | |> send_resp(200, """
19 | {
20 | "app": "CF.AtomFeed",
21 | "status": "✔",
22 | "version": "#{CF.AtomFeed.Application.version()}",
23 | "db_version": "#{DB.Application.version()}"
24 | }
25 | """)
26 | end
27 |
28 | @feed_content_type "application/atom+xml"
29 |
30 | defp render_feed(conn, feed_content) do
31 | conn
32 | |> put_resp_content_type(@feed_content_type)
33 | |> send_resp(200, feed_content)
34 | end
35 |
36 | get "/comments" do
37 | render_feed(conn, CF.AtomFeed.Comments.feed_all())
38 | end
39 |
40 | get "/statements" do
41 | render_feed(conn, CF.AtomFeed.Statements.feed_all())
42 | end
43 |
44 | get "/videos" do
45 | render_feed(conn, CF.AtomFeed.Videos.feed_all())
46 | end
47 |
48 | get "/flags" do
49 | render_feed(conn, CF.AtomFeed.Flags.feed_all())
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/apps/cf_atom_feed/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.AtomFeed.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :cf_atom_feed,
7 | version: "1.0.4",
8 | build_path: "../../_build",
9 | config_path: "../../config/config.exs",
10 | deps_path: "../../deps",
11 | lockfile: "../../mix.lock",
12 | elixir: "~> 1.6",
13 | elixirc_paths: elixirc_paths(Mix.env()),
14 | compilers: Mix.compilers(),
15 | start_permanent: Mix.env() == :prod,
16 | deps: deps(),
17 | test_coverage: [tool: ExCoveralls]
18 | ]
19 | end
20 |
21 | # Configuration for the OTP application.
22 | #
23 | # Type `mix help compile.app` for more information.
24 | def application do
25 | [
26 | mod: {CF.AtomFeed.Application, []},
27 | extra_applications: [:logger, :runtime_tools, :cowboy, :plug]
28 | ]
29 | end
30 |
31 | # Specifies which paths to compile per environment.
32 | defp elixirc_paths(:test), do: ["lib", "test/support"]
33 | defp elixirc_paths(_), do: ["lib"]
34 |
35 | defp deps do
36 | [
37 | # --- Runtime
38 | {:atomex, "~> 0.2"},
39 | {:cowboy, "~> 2.0"},
40 | {:plug, "~> 1.7"},
41 | {:kaur, "~> 1.1"},
42 |
43 | # ---- In Umbrella
44 | {:db, in_umbrella: true},
45 | {:cf, in_umbrella: true}
46 | ]
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/apps/cf_atom_feed/test/comments_test.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.AtomFeed.CommentsTest do
2 | use ExUnit.Case
3 | alias DB.{Repo, Schema, Factory}
4 |
5 | setup do
6 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(DB.Repo)
7 | end
8 |
9 | test "render a basic feed" do
10 | # Ensure comments from previous tests get deleted
11 | Repo.delete_all(Schema.Comment)
12 |
13 | # Insert fake comments and render feed
14 | comments = Factory.insert_list(5, :comment)
15 | feed = CF.AtomFeed.Comments.feed_all()
16 |
17 | # Check feed info
18 | assert feed =~ """
19 |
20 |
21 |
22 |
23 | CaptainFact
24 | atom-feed@captainfact.io
25 |
26 | https://TEST_FRONTEND/
27 | [CaptainFact] All Comments
28 | """
29 |
30 | # Check comment entries
31 | for comment <- comments do
32 | assert feed =~
33 | ~r(https://TEST_FRONTEND/videos/[a-zA-Z0-9]+\?statement=#{comment.statement_id}&c=#{comment.id}"/>)
34 |
35 | assert feed =~ ~r(New Comment from .+ on ##{comment.statement_id})
36 | end
37 | end
38 |
39 | test "should properly render anonymized comments" do
40 | # Ensure comments from previous tests get deleted
41 | Repo.delete_all(Schema.Comment)
42 |
43 | Factory.insert(:comment, user: nil)
44 | feed = CF.AtomFeed.Comments.feed_all()
45 | assert feed =~ ~r(New Comment from Deleted account)
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/apps/cf_atom_feed/test/statements_test.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.AtomFeed.StatementsTest do
2 | use ExUnit.Case
3 | alias DB.{Repo, Schema, Factory}
4 |
5 | setup do
6 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(DB.Repo)
7 | end
8 |
9 | test "render a basic feed" do
10 | # Ensure comments from previous tests get deleted
11 | Repo.delete_all(Schema.Statement)
12 |
13 | # Insert fake comments and render feed
14 | statements = Factory.insert_list(5, :statement)
15 | feed = CF.AtomFeed.Statements.feed_all()
16 |
17 | # Check feed info
18 | assert String.starts_with?(feed, """
19 |
20 |
21 |
22 |
23 | CaptainFact
24 | atom-feed@captainfact.io
25 |
26 | https://TEST_FRONTEND/
27 | [CaptainFact] All Statements
28 | """)
29 |
30 | # Check comment entries
31 | for statement <- statements do
32 | statement_url =
33 | "https://TEST_FRONTEND/videos/#{statement.video.hash_id}?statement=#{statement.id}"
34 |
35 | assert feed =~ statement_url
36 | assert feed =~ "New statement for video #{statement.video.title}"
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/apps/cf_atom_feed/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | Ecto.Adapters.SQL.Sandbox.mode(DB.Repo, {:shared, self()})
2 |
3 | ExUnit.start()
4 |
--------------------------------------------------------------------------------
/apps/cf_graphql/.gitignore:
--------------------------------------------------------------------------------
1 | # App artifacts
2 | /_build
3 | /db
4 | /deps
5 | /*.ez
6 |
7 | # Generated on crash by the VM
8 | erl_crash.dump
9 |
10 |
--------------------------------------------------------------------------------
/apps/cf_graphql/README.md:
--------------------------------------------------------------------------------
1 | # [CaptainFact App] CF GraphQL
2 |
3 | ## Secrets
4 |
5 | Following secrets must be configured in production:
6 |
7 | - db_hostname
8 | - db_username
9 | - db_password
10 | - db_name
11 | - frontend_url
12 | - host
13 |
--------------------------------------------------------------------------------
/apps/cf_graphql/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | #
4 | # This configuration file is loaded before any dependency and
5 | # is restricted to this project.
6 | use Mix.Config
7 |
8 | # General application configuration
9 | config :cf_graphql,
10 | namespace: CF.Graphql,
11 | ecto_repos: [DB.Repo],
12 | env: Mix.env()
13 |
14 | # Configures the endpoint
15 | config :cf_graphql, CF.GraphQLWeb.Endpoint,
16 | url: [host: "localhost"],
17 | secret_key_base: "Nl5lfMlBMvQpY3n74G9iNTxH4okMpbMWArWst9Vhj75tl+m2PuV+KPwjX0fNMaa8",
18 | pubsub_server: CF.Graphql.PubSub,
19 | server: true
20 |
21 | # Configures Elixir's Logger
22 | config :logger, :console,
23 | format: "$time $metadata[$level] $message\n",
24 | metadata: [:request_id]
25 |
26 | # Configure Postgres pool size
27 | config :db, DB.Repo, pool_size: 5
28 |
29 | # Import environment specific config. This must remain at the bottom
30 | # of this file so it overrides the configuration defined above.
31 | import_config "#{Mix.env()}.exs"
32 |
--------------------------------------------------------------------------------
/apps/cf_graphql/config/dev.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | config :cf_graphql, CF.GraphQLWeb.Endpoint,
4 | http: [port: 4002],
5 | https: [
6 | port: 4003,
7 | otp_app: :cf_graphql,
8 | keyfile: "priv/keys/privkey.pem",
9 | certfile: "priv/keys/fullchain.pem"
10 | ],
11 | debug_errors: true,
12 | code_reloader: false,
13 | check_origin: false,
14 | watchers: []
15 |
16 | # Do not include metadata nor timestamps in development logs
17 | config :logger, :console, format: "[$level] $message\n"
18 |
19 | # Set a higher stacktrace during development. Avoid configuring such
20 | # in production as building large stacktraces may be expensive.
21 | config :phoenix, :stacktrace_depth, 20
22 | config :phoenix, :json_library, Jason
23 |
--------------------------------------------------------------------------------
/apps/cf_graphql/config/prod.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # Do not print debug messages in production
4 | config :logger, level: :info
5 |
6 | # Configure endpoint
7 | config :cf_graphql, CF.GraphQLWeb.Endpoint,
8 | server: false,
9 | debug_errors: false,
10 | code_reloader: false,
11 | check_origin: false,
12 | watchers: []
13 |
--------------------------------------------------------------------------------
/apps/cf_graphql/config/test.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # We don't run a server during test. If one is required,
4 | # you can enable the server option below.
5 | config :cf_graphql, CF.GraphQLWeb.Endpoint,
6 | http: [port: 4001],
7 | server: false
8 |
9 | # Print only warnings and errors during test
10 | config :logger, level: :warn
11 |
--------------------------------------------------------------------------------
/apps/cf_graphql/lib/application.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Graphql.Application do
2 | use Application
3 |
4 | # See https://hexdocs.pm/elixir/Application.html
5 | # for more information on OTP Applications
6 | def start(_type, _args) do
7 | import Supervisor.Spec
8 |
9 | # Define workers and child supervisors to be supervised
10 | children = [
11 | # Start the PubSub system
12 | {Phoenix.PubSub, name: CF.Graphql.PubSub},
13 | # Start the endpoint when the application starts
14 | supervisor(CF.GraphQLWeb.Endpoint, [])
15 | ]
16 |
17 | # See https://hexdocs.pm/elixir/Supervisor.html
18 | # for other strategies and supported options
19 | opts = [strategy: :one_for_one, name: CF.Graphql.Supervisor]
20 | Supervisor.start_link(children, opts)
21 | end
22 |
23 | # Tell Phoenix to update the endpoint configuration
24 | # whenever the application is updated.
25 | def config_change(changed, _new, removed) do
26 | CF.GraphQLWeb.Endpoint.config_change(changed, removed)
27 | :ok
28 | end
29 |
30 | def version() do
31 | case :application.get_key(:cf_graphql, :vsn) do
32 | {:ok, version} -> to_string(version)
33 | _ -> "unknown"
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/apps/cf_graphql/lib/auth_pipeline.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Graphql.AuthPipeline do
2 | @moduledoc """
3 | Adds the token authentification from CF app to graphql API.
4 | """
5 |
6 | @behaviour Plug
7 |
8 | import Plug.Conn
9 |
10 | def init(opts), do: opts
11 |
12 | def call(conn, _) do
13 | case build_context(conn) do
14 | {:ok, context} ->
15 | put_private(conn, :absinthe, %{context: context})
16 |
17 | _ ->
18 | conn
19 | end
20 | end
21 |
22 | defp build_context(conn) do
23 | with ["Bearer " <> token] <- get_req_header(conn, "authorization"),
24 | {:ok, current_user, _claims} <- authorize(token) do
25 | {:ok, %{user: current_user}}
26 | end
27 | end
28 |
29 | defp authorize(token) do
30 | CF.Authenticator.GuardianImpl.resource_from_token(token)
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/apps/cf_graphql/lib/captain_fact_graphql_web.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.GraphQLWeb do
2 | @moduledoc """
3 | The entrypoint for defining your web interface, such
4 | as controllers, views, channels and so on.
5 |
6 | This can be used in your application as:
7 |
8 | use CF.GraphQLWeb, :controller
9 | use CF.GraphQLWeb, :view
10 |
11 | The definitions below will be executed for every view,
12 | controller, etc, so keep them short and clean, focused
13 | on imports, uses and aliases.
14 |
15 | Do NOT define functions inside the quoted expressions
16 | below. Instead, define any helper function in modules
17 | and import those modules here.
18 | """
19 |
20 | def router do
21 | quote do
22 | use Phoenix.Router
23 | import Plug.Conn
24 | import Phoenix.Controller
25 | end
26 | end
27 |
28 | @doc """
29 | When used, dispatch to the appropriate controller/view/etc.
30 | """
31 | defmacro __using__(which) when is_atom(which) do
32 | apply(__MODULE__, which, [])
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/apps/cf_graphql/lib/endpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.GraphQLWeb.Endpoint do
2 | use Phoenix.Endpoint, otp_app: :cf_graphql
3 |
4 | plug(Plug.RequestId)
5 | plug(Plug.Logger)
6 |
7 | plug(
8 | Corsica,
9 | max_age: 3600,
10 | allow_headers: ~w(Accept Content-Type Authorization Origin),
11 | origins: "*"
12 | )
13 |
14 | plug(
15 | Plug.Parsers,
16 | parsers: [:urlencoded, :multipart, :json, Absinthe.Plug.Parser],
17 | pass: ["*/*"],
18 | json_decoder: Jason
19 | )
20 |
21 | plug(Plug.MethodOverride)
22 | plug(Plug.Head)
23 | plug(CF.GraphQLWeb.Router)
24 | end
25 |
--------------------------------------------------------------------------------
/apps/cf_graphql/lib/resolvers/app_info.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Graphql.Resolvers.AppInfo do
2 | def info(_, _args, _info) do
3 | {:ok,
4 | %{
5 | app: "CF.Graphql",
6 | status: "✔",
7 | version: CF.Graphql.Application.version(),
8 | db_version: DB.Application.version()
9 | }}
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/apps/cf_graphql/lib/resolvers/comments.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Graphql.Resolvers.Comments do
2 | import Absinthe.Resolution.Helpers, only: [batch: 3]
3 | import Ecto.Query
4 | alias DB.Repo
5 | alias DB.Schema.Vote
6 |
7 | def score(comment, _args, _info) do
8 | batch({__MODULE__, :comments_scores}, comment.id, fn results ->
9 | {:ok, Map.get(results, comment.id) || 0}
10 | end)
11 | end
12 |
13 | def comments_scores(_, comments_ids) do
14 | Vote
15 | |> where([v], v.comment_id in ^comments_ids)
16 | |> select([v], {v.comment_id, sum(v.value)})
17 | |> group_by([v], v.comment_id)
18 | |> Repo.all()
19 | |> Enum.into(%{})
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/apps/cf_graphql/lib/resolvers/speakers.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Graphql.Resolvers.Speakers do
2 | def picture(speaker, _, _) do
3 | {:ok, DB.Type.SpeakerPicture.full_url(speaker, :thumb)}
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/apps/cf_graphql/lib/resolvers/statements.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Graphql.Resolvers.Statements do
2 | @moduledoc """
3 | Resolver for `DB.Schema.Statement`
4 | """
5 |
6 | alias Kaur.Result
7 |
8 | import Ecto.Query
9 | import Absinthe.Resolution.Helpers, only: [batch: 3]
10 |
11 | alias DB.Repo
12 | alias DB.Schema.Statement
13 |
14 | # Queries
15 |
16 | def paginated_list(_root, args = %{offset: offset, limit: limit}, _info) do
17 | Statement
18 | |> Statement.query_list(Map.get(args, :filters, []))
19 | |> Repo.paginate(page: offset, page_size: limit)
20 | |> Result.ok()
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/apps/cf_graphql/lib/resolvers/statistics.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Graphql.Resolvers.Statistics do
2 | @moduledoc """
3 | Absinthe solver for community insights and statistics
4 | """
5 |
6 | alias DB.Statistics
7 |
8 | alias Kaur.Result
9 |
10 | @doc """
11 | Get default statistic object
12 | """
13 | @spec default(any, any, any) :: Result.result_tuple()
14 | def default(_, _, _) do
15 | Result.ok(%{})
16 | end
17 |
18 | @doc """
19 | Solvers for statistics
20 | """
21 | @spec all_totals(any, any, any) :: Result.result_tuple()
22 | def all_totals(_, _, _) do
23 | Result.ok(Statistics.all_totals())
24 | end
25 |
26 | @doc """
27 | returns
28 | `{:ok, best_users}`
29 | `{:error, "leaderboard unaccessible"}
30 | """
31 | @spec leaderboard(any, any) :: {:ok, list} | {:error, binary}
32 | def leaderboard(_root, _args) do
33 | Statistics.leaderboard()
34 | |> Result.from_value()
35 | |> Result.map_error(fn _ -> "leaderboard unaccessible" end)
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/apps/cf_graphql/lib/router.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.GraphQLWeb.Router do
2 | use CF.GraphQLWeb, :router
3 |
4 | @graphiql_route "/graphiql"
5 |
6 | pipeline :api do
7 | plug(:accepts, ["json"])
8 | end
9 |
10 | pipeline :api_auth do
11 | plug(:accepts, ["json"])
12 | plug(CF.Graphql.AuthPipeline)
13 | end
14 |
15 | scope "/" do
16 | pipe_through(:api_auth)
17 |
18 | scope @graphiql_route do
19 | forward(
20 | "/",
21 | Absinthe.Plug.GraphiQL,
22 | schema: CF.Graphql.Schema,
23 | analyze_complexity: true,
24 | max_complexity: 400
25 | )
26 | end
27 |
28 | forward(
29 | "/",
30 | Absinthe.Plug,
31 | schema: CF.Graphql.Schema,
32 | analyze_complexity: true,
33 | max_complexity: 400
34 | )
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/apps/cf_graphql/lib/schema/input_objects/statement_filter.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Graphql.Schema.InputObjects.StatementFilter do
2 | @moduledoc """
3 | Represent the possible filters to apply to statement.
4 | """
5 |
6 | use Absinthe.Schema.Notation
7 |
8 | @desc "Props to filter statements on"
9 | input_object :statement_filter do
10 | field(:commented, :boolean)
11 | field(:is_draft, :boolean)
12 | field(:speaker_id, :id)
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/apps/cf_graphql/lib/schema/input_objects/video_filter.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Graphql.Schema.InputObjects.VideoFilter do
2 | @moduledoc """
3 | Represent a user's Notification.
4 | """
5 |
6 | use Absinthe.Schema.Notation
7 |
8 | @desc "Props to filter videos on"
9 | input_object :video_filter do
10 | field(:language, :string)
11 | field(:min_id, :id)
12 | field(:speaker_id, :id)
13 | field(:speaker_slug, :string)
14 | field(:is_partner, :boolean)
15 | field(:is_featured, :boolean)
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/apps/cf_graphql/lib/schema/middleware/require_authentication.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Graphql.Schema.Middleware.RequireAuthentication do
2 | @moduledoc """
3 | A middleware to force authentication.
4 | """
5 |
6 | @behaviour Absinthe.Middleware
7 |
8 | @doc false
9 | def call(resolution, _args) do
10 | if is_nil(resolution.context[:user]) do
11 | Absinthe.Resolution.put_result(resolution, {:error, "unauthorized"})
12 | else
13 | resolution
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/apps/cf_graphql/lib/schema/middleware/require_reputation.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Graphql.Schema.Middleware.RequireReputation do
2 | @moduledoc """
3 | A middleware to ensure the user has a certain reputation.
4 | """
5 |
6 | @behaviour Absinthe.Middleware
7 |
8 | @doc false
9 | def call(resolution, reputation) do
10 | cond do
11 | is_nil(resolution.context[:user]) ->
12 | Absinthe.Resolution.put_result(resolution, {:error, "unauthorized"})
13 |
14 | resolution.context[:user].reputation && resolution.context[:user].reputation < reputation ->
15 | Absinthe.Resolution.put_result(
16 | resolution,
17 | {:error,
18 | %{
19 | code: "unauthorized",
20 | message: "You do not have the required reputation to perform this action.",
21 | details: %{
22 | user_reputation: resolution.context[:user].reputation,
23 | required_reputation: reputation
24 | }
25 | }}
26 | )
27 |
28 | true ->
29 | resolution
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/apps/cf_graphql/lib/schema/types/app_info.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Graphql.Schema.Types.AppInfo do
2 | @moduledoc """
3 | App info representation. Contains version, status...etc
4 | """
5 |
6 | use Absinthe.Schema.Notation
7 |
8 | @desc "Information about the application"
9 | object :app_info do
10 | @desc "Indicate if the application is running properly with a checkmark"
11 | field(:status, non_null(:string))
12 | @desc "Graphql API version"
13 | field(:version, non_null(:string))
14 | @desc "Version of the database app attached to this API"
15 | field(:db_version, non_null(:string))
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/apps/cf_graphql/lib/schema/types/comment.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Graphql.Schema.Types.Comment do
2 | @moduledoc """
3 | Representation of a `DB.Schema.Comment` for Absinthe
4 | """
5 |
6 | use Absinthe.Schema.Notation
7 |
8 | import Absinthe.Resolution.Helpers, only: [dataloader: 1]
9 | import CF.Graphql.Schema.Utils
10 | alias CF.Graphql.Resolvers
11 |
12 | @desc "A user's comment. A comment will be considered being a fact if it has a source"
13 | object :comment do
14 | field(:id, non_null(:id))
15 | @desc "User who made the comment"
16 | field :user, :user do
17 | resolve(dataloader(DB.Repo))
18 | complexity(join_complexity())
19 | end
20 |
21 | @desc "Text of the comment. Can be null if the comment has a source"
22 | field(:text, :string)
23 | @desc "Can be true / false (facts) or null (comment)"
24 | field(:approve, :boolean)
25 | @desc "Datetime at which the comment has been added"
26 | field(:inserted_at, :string)
27 | @desc "Score of the comment / fact, based on users votes"
28 | field :score, non_null(:integer) do
29 | resolve(&Resolvers.Comments.score/3)
30 | complexity(join_complexity())
31 | end
32 |
33 | @desc "Source of the scomment. If null, a text must be set"
34 | field :source, :source do
35 | resolve(dataloader(DB.Repo))
36 | complexity(join_complexity())
37 | end
38 |
39 | @desc "If this comment is a reply, this will point toward the comment being replied to"
40 | field(:reply_to_id, :id)
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/apps/cf_graphql/lib/schema/types/notification.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Graphql.Schema.Types.Notification do
2 | @moduledoc """
3 | Represent a user's Notification.
4 | """
5 |
6 | use Absinthe.Schema.Notation
7 |
8 | import Absinthe.Resolution.Helpers, only: [dataloader: 1]
9 | import CF.Graphql.Schema.Utils
10 |
11 | @desc "A user notification"
12 | object :notification do
13 | field(:id, non_null(:id))
14 | @desc "Type of the notification"
15 | field(:type, non_null(:string))
16 | @desc "Notification creation datetime"
17 | field(:inserted_at, non_null(:string))
18 | @desc "When the notification has been seen, or null if it has not"
19 | field(:seen_at, :string)
20 | @desc "Action the notification is referencing"
21 | field :action, :user_action do
22 | resolve(dataloader(DB.Repo))
23 | complexity(join_complexity())
24 | end
25 | end
26 |
27 | @desc "A paginated list of user actions"
28 | object :paginated_notifications do
29 | import_fields(:paginated)
30 | field(:entries, list_of(:notification))
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/apps/cf_graphql/lib/schema/types/paginated.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Graphql.Schema.Types.Paginated do
2 | @moduledoc """
3 | A generic pagination object
4 | """
5 |
6 | use Absinthe.Schema.Notation
7 |
8 | object :paginated do
9 | field(:page_number, :integer)
10 | field(:page_size, :integer)
11 | field(:total_pages, :integer)
12 | field(:total_entries, :integer)
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/apps/cf_graphql/lib/schema/types/source.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Graphql.Schema.Types.Source do
2 | @moduledoc """
3 | Representation of a `DB.Schema.Source` for Absinthe
4 | """
5 |
6 | use Absinthe.Schema.Notation
7 |
8 | @desc "An URL pointing toward a source (article, video, pdf...)"
9 | object :source do
10 | @desc "Unique id of the source"
11 | field(:id, non_null(:id))
12 | @desc "URL of the source"
13 | field(:url, non_null(:string))
14 | @desc "Title of the page / article"
15 | field(:title, :string)
16 | @desc "Language of the page / article"
17 | field(:language, :string)
18 | @desc "Site name extracted from OpenGraph"
19 | field(:site_name, :string)
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/apps/cf_graphql/lib/schema/types/speaker.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Graphql.Schema.Types.Speaker do
2 | @moduledoc """
3 | Representation of a `DB.Schema.Speaker` for Absinthe
4 | """
5 |
6 | use Absinthe.Schema.Notation
7 |
8 | import Absinthe.Resolution.Helpers, only: [dataloader: 1]
9 | import Absinthe.Resolution.Helpers, only: [dataloader: 1]
10 | import CF.Graphql.Schema.Utils
11 | alias CF.Graphql.Resolvers
12 |
13 | @desc "A speaker appearing in one or more videos"
14 | object :speaker do
15 | field(:id, non_null(:id))
16 | @desc "A unique slug to identify the speaker"
17 | field(:slug, :string)
18 | @desc "Full name"
19 | field(:full_name, non_null(:string))
20 |
21 | @desc "Official title (can have multiple separated by a comma). Ex: Politician, activist, writer"
22 | field(:title, :string)
23 | @desc "Country code of the speaker's origin (from wikidata)"
24 | field(:country, :string)
25 | @desc "Wikidata unique identifier, without the 'Q' prefix"
26 | field(:wikidata_item_id, :string)
27 | @desc "Speaker's picture URL. Format is 50x50"
28 | field(:picture, :string, do: resolve(&Resolvers.Speakers.picture/3))
29 | @desc "List of speaker's videos"
30 | field :videos, list_of(:video) do
31 | resolve(dataloader(DB.Repo))
32 | complexity(join_complexity())
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/apps/cf_graphql/lib/schema/types/statement.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Graphql.Schema.Types.Statement do
2 | @moduledoc """
3 | Representation of a `DB.Schema.Statement` for Absinthe
4 | """
5 |
6 | use Absinthe.Schema.Notation
7 |
8 | import Absinthe.Resolution.Helpers, only: [dataloader: 1]
9 | import CF.Graphql.Schema.Utils
10 |
11 | @desc "A transcript or a description of the picture"
12 | object :statement do
13 | field(:id, non_null(:id))
14 | @desc "Speaker's transcript or image description"
15 | field(:text, non_null(:string))
16 | @desc "Statement timecode, in seconds"
17 | field(:time, non_null(:integer))
18 | @desc "Whether the statement is in draft mode"
19 | field(:is_draft, non_null(:boolean))
20 |
21 | @desc "Statement's speaker. Null if statement describes picture"
22 | field :speaker, :speaker do
23 | resolve(dataloader(DB.Repo))
24 | complexity(join_complexity())
25 | end
26 |
27 | @desc "List of users comments and facts for this statement"
28 | field :comments, list_of(:comment) do
29 | resolve(dataloader(DB.Repo))
30 | complexity(join_complexity())
31 | end
32 |
33 | @desc "The video associated with this statement"
34 | field :video, :video do
35 | resolve(dataloader(DB.Repo))
36 | complexity(join_complexity())
37 | end
38 | end
39 |
40 | @desc "A list a paginated statements"
41 | object :paginated_statements do
42 | import_fields(:paginated)
43 | field(:entries, list_of(:statement))
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/apps/cf_graphql/lib/schema/types/statistics.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Graphql.Schema.Types.Statistics do
2 | @moduledoc """
3 | Various application statistics, like the number of users, the number of
4 | comments...
5 | """
6 |
7 | use Absinthe.Schema.Notation
8 | alias CF.Graphql.Resolvers
9 |
10 | @desc "Statistics about the platform community"
11 | object :statistics do
12 | @desc "All totals"
13 | field(:totals, :statistic_totals, do: resolve(&Resolvers.Statistics.all_totals/3))
14 | @desc "List the 20 best users"
15 | field(:leaderboard, list_of(:user), do: resolve(&Resolvers.Statistics.leaderboard/2))
16 | end
17 |
18 | @desc "Counts for all public CF tables"
19 | object :statistic_totals do
20 | field(:users, non_null(:integer))
21 | field(:comments, non_null(:integer))
22 | field(:statements, non_null(:integer))
23 | field(:sources, non_null(:integer))
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/apps/cf_graphql/lib/schema/types/video_caption.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Graphql.Schema.Types.VideoCaption do
2 | @moduledoc """
3 | A single caption for a video
4 | """
5 |
6 | use Absinthe.Schema.Notation
7 |
8 | @desc "Information about the application"
9 | object :video_caption do
10 | @desc "Caption text"
11 | field(:text, non_null(:string))
12 | @desc "Caption start time (in seconds)"
13 | field(:start, non_null(:float))
14 | @desc "Caption duration (in seconds)"
15 | field(:duration, :float)
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/apps/cf_graphql/lib/schema/utils.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Graphql.Schema.Utils do
2 | @moduledoc """
3 | Utility functions for types and resolvers.
4 | """
5 |
6 | @default_join_complexity 50
7 |
8 | @doc """
9 | Sets the join complexity for given association. Default join complexity is
10 | set in @default_join_complexity which value is `50`
11 | """
12 | defmacro join_complexity(complexity \\ @default_join_complexity) do
13 | quote do
14 | fn _, child_complexity -> unquote(complexity) + child_complexity end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/apps/cf_graphql/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.Graphql.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :cf_graphql,
7 | version: "1.1.0",
8 | build_path: "../../_build",
9 | config_path: "../../config/config.exs",
10 | deps_path: "../../deps",
11 | lockfile: "../../mix.lock",
12 | elixir: "~> 1.6",
13 | elixirc_paths: elixirc_paths(Mix.env()),
14 | compilers: [:phoenix] ++ Mix.compilers(),
15 | build_embedded: Mix.env() == :prod,
16 | start_permanent: Mix.env() == :prod,
17 | aliases: aliases(),
18 | deps: deps(),
19 | test_coverage: [tool: ExCoveralls]
20 | ]
21 | end
22 |
23 | def application do
24 | [
25 | mod: {CF.Graphql.Application, []},
26 | extra_applications: [:logger, :runtime_tools]
27 | ]
28 | end
29 |
30 | defp elixirc_paths(:test), do: ["lib", "test/support"]
31 | defp elixirc_paths(_), do: ["lib"]
32 |
33 | defp deps do
34 | [
35 | {:phoenix, "~> 1.5.14"},
36 | {:phoenix_pubsub, "~> 2.0"},
37 | {:jason, "~> 1.4"},
38 | {:plug, "~> 1.7"},
39 | {:cowboy, "~> 2.0"},
40 | {:corsica, "~> 2.1"},
41 | {:absinthe_plug, "~> 1.5"},
42 | {:dataloader, "~> 2.0.2"},
43 | {:kaur, "~> 1.1"},
44 | {:poison, "~> 3.1"},
45 |
46 | # Internal dependencies
47 | {:db, in_umbrella: true},
48 | {:cf, in_umbrella: true},
49 |
50 | # Dev only
51 | {:exsync, "~> 0.2", only: :dev}
52 | ]
53 | end
54 |
55 | defp aliases do
56 | []
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/apps/cf_graphql/priv/keys/fullchain.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIC+zCCAeOgAwIBAgIJAP5GvLkEg+P4MA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV
3 | BAMMCWxvY2FsaG9zdDAeFw0xNzA1MTIwNjEwMzJaFw0yNzA1MTAwNjEwMzJaMBQx
4 | EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
5 | ggEBAL6uymshViiULeyiTXOUYMppzagH4W+bCA75rYyYFUXJJbvvaE3QVvpAyTIz
6 | OXX1rNCvoxxUknV0d3y3JyqN8fU+Oi7Q5rkliZu9tN6xthJrXgRnm12HomNRlC2i
7 | rIXhLTJVcPGM7kbiPZOVKCkN0MF+9EPEvnq9xvtDgzc3ZBKnvUglKrjdwUCZJMTQ
8 | fggtN1PLbNpKGLx3cWBb3SRuzwtVLi34ixMgwnyXzMreH5U1IK7K/hra9vhB1N7j
9 | npx4vlNVagzFZuybuf4Aozne3yioU1z/8sAnHb83DoGs+JnRAnGSZyzg/eGIqdCd
10 | GMTsWH3CHnetE5pNGcwJuL4PFE8CAwEAAaNQME4wHQYDVR0OBBYEFK7VtLoCcnIN
11 | /0YYwfD9iTfTkm3OMB8GA1UdIwQYMBaAFK7VtLoCcnIN/0YYwfD9iTfTkm3OMAwG
12 | A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH/mDv1G3XKdfbzXiPZNiqVa
13 | N17yaffJLFA5Kj33RVwChhutZi0CZYYAuY19BkW9j4+poz2xnRhcvkJqgBdL0mvu
14 | 0NSnCXI/S8+Im97h2aUGgbUQvOxDaeLpLFWt7zZAT9y5zBB4PjwMJY/pp5KAtcNc
15 | GGyfSsr8jhlcriBUqrYUrgX8AvDV8qM2Y++nJ8igmTVjDgWG8hAuipHRmH8r6PBK
16 | 1XeTf4+Q3WB99kEcTglfPgm68KGavRimmuuUejShmulzbNiT+OEMO1KHPIfOc/z9
17 | 4AobuD/k0WNGqOx9Y27A7t7ldKxB7ByXvEwetxyIc3Q9Pv0hrPVGs5ceR1uCk4s=
18 | -----END CERTIFICATE-----
19 |
--------------------------------------------------------------------------------
/apps/cf_graphql/priv/keys/privkey.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEpAIBAAKCAQEAvq7KayFWKJQt7KJNc5RgymnNqAfhb5sIDvmtjJgVRcklu+9o
3 | TdBW+kDJMjM5dfWs0K+jHFSSdXR3fLcnKo3x9T46LtDmuSWJm7203rG2EmteBGeb
4 | XYeiY1GULaKsheEtMlVw8YzuRuI9k5UoKQ3QwX70Q8S+er3G+0ODNzdkEqe9SCUq
5 | uN3BQJkkxNB+CC03U8ts2koYvHdxYFvdJG7PC1UuLfiLEyDCfJfMyt4flTUgrsr+
6 | Gtr2+EHU3uOenHi+U1VqDMVm7Ju5/gCjOd7fKKhTXP/ywCcdvzcOgaz4mdECcZJn
7 | LOD94Yip0J0YxOxYfcIed60Tmk0ZzAm4vg8UTwIDAQABAoIBACNDf/u/9ocaoEOa
8 | 4Gf3kM7eMkJY8sAJE7xxQD84APce8/OFmuyJEwzE3nCCOKYwAP22/ZtHqK5AE7jk
9 | xkGAbrbEA06VI5Yp8wDyXHiytNFDOefmoTzy0H09oQGvi+hWdF1Sn8iMH6TMQkcA
10 | 1qSBAZJHQDUoNXHNlvbwzVtwyvkH6gXOy1xzPJLWT/9z0TYZN25QJ7hjxVygrqxk
11 | NdDL0J77lBncuMMwTUnUrElJNPEJMaJnINRFs/EpOUZbvFa3sczOfG0JGoJA8Rra
12 | 6jqh4UE5g/ruYR499KaN5zL5Y0w8JeLyglwhGARnEvGl88gpNqBs7KHlEJBhX0jv
13 | 7sEn+kECgYEA+lJdZkC6rNlRiFurJG06AVOL3hej7x1Int/htdVze40OLKlf5Mii
14 | E0Bqp6meRpk8B0IMkhrN8/LN00YV6ktWhRFUBNN0DCCxRVXk1a9fJrvSoDGkVg2Q
15 | Xnd6RjPyW7ub8L5QoUYSuzfcEyULdeWAi7eRDJa0Efaj2AWNYQqQfVsCgYEAwwIZ
16 | MIvEmZkRI/o3aqGoaAcvYJ4+y9TKNhdQCBxsV31YAejzgPLxPaM1+LtoxwZ9Ls8S
17 | gQzskE9bHXylxNMpJApn8DeuIfMLrFa+7VxIC38Gl97C8MAgZvqhSWIpqsY5avBr
18 | D29vYHSidrfioWRM5xmXXO3ys8t/shigANLzcx0CgYEA5LzK2Bsh+byDgmSxmJGu
19 | xXOAhat4g5FwwKy35Z5s7mNQpoMHO1oSsCDW1Opr1PtFHSS/s+qGc/pVFlAeyn+Z
20 | SfMxoU9P5Z0iH8eDWbfs7MoIh5WVI4U1fP0UYH4rYqOmtXBS4WvUxfsfQOdC97KF
21 | qiZNhwFW/mswAL/iFuC+c60CgYBcw/rHpTV4+9+zhawnBY/fLMvU4nJs9GTdJmnj
22 | 8eF4HSBoiDCN/wPTlnhuQnitdODIC6l5ynQekiF9/XW+E9VWV7zqARLNA5lh+kIJ
23 | GAUNsven90g0zrCbTE69Yf0ASBu4S3YieZg6AkHmx8L/k38h0IK4qljyPrQYPK6g
24 | tbkp4QKBgQCKYtF5GvQs2ZCfjYepYuTYzeSOrpy8jnbytlU//KSrzc+htvAuXK70
25 | pwBRilKRlt7h8g1RS+OJi/H8LoOd+sP+lhaEWtHkGGnJTPXfqQ0ETNAeXmUJNP0N
26 | /Auxa38OOGm7owhiKiAIzlE9EwN+7MHcNoaVoBrqT5YtlEPrzAO+/A==
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/apps/cf_graphql/priv/secrets/basic_auth_password:
--------------------------------------------------------------------------------
1 | captain
--------------------------------------------------------------------------------
/apps/cf_graphql/priv/secrets/host:
--------------------------------------------------------------------------------
1 | localhost
--------------------------------------------------------------------------------
/apps/cf_graphql/priv/secrets/secret_key_base:
--------------------------------------------------------------------------------
1 | 3q0jsoW4rL+K6iO7LDeJXeI4bck9DuTKNkMOc1+k1cgcAcwz0DSXVPfLr1p1gP4H
--------------------------------------------------------------------------------
/apps/cf_graphql/test/support/conn_case.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Graphql.ConnCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | tests that require setting up a connection.
5 |
6 | Finally, if the test case interacts with the database,
7 | it cannot be async. For this reason, every test runs
8 | inside a transaction which is reset at the beginning
9 | of the test unless the test case is marked as async.
10 | """
11 |
12 | use ExUnit.CaseTemplate
13 |
14 | using do
15 | quote do
16 | # Import conveniences for testing with connections
17 | import Plug.Conn
18 | import Phoenix.ConnTest
19 | import CF.GraphQLWeb.Router.Helpers
20 |
21 | # The default endpoint for testing
22 | @endpoint CF.GraphQLWeb.Endpoint
23 | end
24 | end
25 |
26 | setup tags do
27 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(DB.Repo)
28 |
29 | unless tags[:async] do
30 | Ecto.Adapters.SQL.Sandbox.mode(DB.Repo, {:shared, self()})
31 | end
32 |
33 | {:ok, conn: Phoenix.ConnTest.build_conn()}
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/apps/cf_graphql/test/support/data_case.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Graphql.DataCase do
2 | @moduledoc """
3 | This module defines the setup for tests requiring
4 | access to the application's data layer.
5 |
6 | You may define functions here to be used as helpers in
7 | your tests.
8 |
9 | Finally, if the test case interacts with the database,
10 | it cannot be async. For this reason, every test runs
11 | inside a transaction which is reset at the beginning
12 | of the test unless the test case is marked as async.
13 | """
14 |
15 | use ExUnit.CaseTemplate
16 |
17 | using do
18 | quote do
19 | alias DB.Repo
20 |
21 | import Ecto
22 | import Ecto.Changeset
23 | import Ecto.Query
24 | import CF.Graphql.DataCase
25 | end
26 | end
27 |
28 | setup tags do
29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(DB.Repo)
30 |
31 | unless tags[:async] do
32 | Ecto.Adapters.SQL.Sandbox.mode(DB.Repo, {:shared, self()})
33 | end
34 |
35 | :ok
36 | end
37 |
38 | @doc """
39 | A helper that transform changeset errors to a map of messages.
40 |
41 | assert {:error, changeset} = Accounts.create_user(%{password: "short"})
42 | assert "password is too short" in errors_on(changeset).password
43 | assert %{password: ["password is too short"]} = errors_on(changeset)
44 |
45 | """
46 | def errors_on(changeset) do
47 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
48 | Enum.reduce(opts, message, fn {key, value}, acc ->
49 | String.replace(acc, "%{#{key}}", to_string(value))
50 | end)
51 | end)
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/apps/cf_graphql/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | Ecto.Adapters.SQL.Sandbox.mode(DB.Repo, {:shared, self()})
2 | ExUnit.start()
3 |
--------------------------------------------------------------------------------
/apps/cf_jobs/README.md:
--------------------------------------------------------------------------------
1 | # [CaptainFact App] CF Jobs
2 |
3 | CaptainFact jobs.
4 |
5 | - Flags: Analyze flags and ban comments when there are too much flags on them.
6 | - Moderation: Analyze moderation feedbacks and take action when a consensus is reached.
7 | - Reputation: Analyze actions to update reputation, taking care of daily limits.
8 |
9 | ## Secrets
10 |
11 | Following secrets must be configured in production:
12 |
13 | - db_hostname
14 | - db_username
15 | - db_password
16 | - db_name
17 | - frontend_url
18 |
--------------------------------------------------------------------------------
/apps/cf_jobs/config/config.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # Configure scheduler
4 | config :cf_jobs, CF.Jobs.Scheduler,
5 | # Run only one instance across cluster
6 | global: true,
7 | debug_logging: false,
8 | jobs: [
9 | # Reputation
10 | update_reputations: [
11 | # every 20 minutes
12 | schedule: {:extended, "*/20"},
13 | task: {CF.Jobs.Reputation, :update, []},
14 | overlap: false
15 | ],
16 | reset_daily_reputation_limits: [
17 | schedule: "@daily",
18 | task: {CF.Jobs.Reputation, :reset_daily_limits, []},
19 | overlap: false
20 | ],
21 | # Moderation
22 | update_moderation: [
23 | # every 5 minutes
24 | schedule: "*/5 * * * *",
25 | task: {CF.Jobs.Moderation, :update, []},
26 | overlap: false
27 | ],
28 | # Flags
29 | update_flags: [
30 | # every minute
31 | schedule: "*/1 * * * *",
32 | task: {CF.Jobs.Flags, :update, []},
33 | overlap: false
34 | ],
35 | # Notifications
36 | create_notifications: [
37 | # every 5 seconds
38 | schedule: {:extended, "*/5"},
39 | task: {CF.Jobs.CreateNotifications, :update, []},
40 | overlap: false
41 | ],
42 | # Captions
43 | download_captions: [
44 | # every 8h
45 | schedule: "0 */8 * * *",
46 | task: {CF.Jobs.DownloadCaptions, :update, []},
47 | overlap: false
48 | ]
49 | ]
50 |
51 | # Configure Postgres pool size
52 | config :db, DB.Repo, pool_size: 3
53 |
54 | # Import environment specific config
55 | import_config "#{Mix.env()}.exs"
56 |
--------------------------------------------------------------------------------
/apps/cf_jobs/config/dev.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
--------------------------------------------------------------------------------
/apps/cf_jobs/config/prod.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
--------------------------------------------------------------------------------
/apps/cf_jobs/config/test.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # Disable CRON tasks on test
4 | config :cf_jobs, CF.Jobs.Scheduler, jobs: []
5 |
--------------------------------------------------------------------------------
/apps/cf_jobs/lib/application.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Jobs.Application do
2 | use Application
3 |
4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html
5 | # for more information on OTP Applications
6 | def start(_type, _args) do
7 | import Supervisor.Spec
8 |
9 | # Wait 10s before starting to give some time for the migrations to run
10 | :timer.sleep(1000)
11 |
12 | env = Application.get_env(:cf, :env)
13 |
14 | # Define workers and child supervisors to be supervised
15 | children = [
16 | # Jobs
17 | worker(CF.Jobs.Reputation, []),
18 | worker(CF.Jobs.Flags, []),
19 | worker(CF.Jobs.Moderation, []),
20 | worker(CF.Jobs.CreateNotifications, []),
21 | worker(CF.Jobs.DownloadCaptions, [])
22 | ]
23 |
24 | # Do not start scheduler in tests
25 | children =
26 | if env == :test or Application.get_env(:cf, :disable_scheduler),
27 | do: children,
28 | else: children ++ [worker(CF.Jobs.Scheduler, [])]
29 |
30 | opts = [strategy: :one_for_one, name: CF.Jobs.Supervisor]
31 | Supervisor.start_link(children, opts)
32 | end
33 |
34 | @doc """
35 | Get app's version from `mix.exs`
36 | """
37 | def version() do
38 | case :application.get_key(:cf, :vsn) do
39 | {:ok, version} -> to_string(version)
40 | _ -> "unknown"
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/apps/cf_jobs/lib/job.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Jobs.Job do
2 | @moduledoc """
3 | Define the common behaviour between jobs.
4 | """
5 |
6 | @type t :: module
7 |
8 | use GenServer
9 |
10 | def init(args) do
11 | {:ok, args}
12 | end
13 |
14 | @doc """
15 | Get the Job name.
16 | """
17 | @callback name() :: atom()
18 | end
19 |
--------------------------------------------------------------------------------
/apps/cf_jobs/lib/jobs/download_captions.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Jobs.DownloadCaptions do
2 | @behaviour CF.Jobs.Job
3 |
4 | require Logger
5 | import Ecto.Query
6 |
7 | alias DB.Repo
8 | alias DB.Schema.Video
9 | alias DB.Schema.VideoCaption
10 | alias DB.Schema.UsersActionsReport
11 |
12 | @name :download_captions
13 | @analyser_id UsersActionsReport.analyser_id(@name)
14 |
15 | # --- Client API ---
16 |
17 | def name, do: @name
18 |
19 | def start_link() do
20 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
21 | end
22 |
23 | def init(args) do
24 | {:ok, args}
25 | end
26 |
27 | # 2 minutes
28 | @timeout 120_000
29 | def update() do
30 | GenServer.call(__MODULE__, :download_captions, @timeout)
31 | end
32 |
33 | # --- Server callbacks ---
34 | def handle_call(:download_captions, _from, _state) do
35 | get_videos()
36 | |> Enum.map(fn video ->
37 | Logger.info("Downloading captions for video #{video.id}")
38 | CF.Videos.download_captions(video)
39 | Process.sleep(1000)
40 | end)
41 |
42 | {:reply, :ok, :ok}
43 | end
44 |
45 | # Get all videos that need new captions. We fetch new captions:
46 | # - For any videos that doesn't have any captions yet
47 | # - For videos whose captions haven't been updated in the last 30 days
48 | defp get_videos() do
49 | Repo.all(
50 | from(v in Video,
51 | limit: 5,
52 | left_join: captions in VideoCaption,
53 | on: captions.video_id == v.id,
54 | where:
55 | is_nil(captions.id) or
56 | captions.updated_at < ^DateTime.add(DateTime.utc_now(), -30 * 24 * 60 * 60, :second),
57 | group_by: v.id,
58 | order_by: [desc: v.inserted_at]
59 | )
60 | )
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/apps/cf_jobs/lib/scheduler.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Jobs.Scheduler do
2 | use Quantum.Scheduler, otp_app: :cf_jobs
3 |
4 | # Scheduler (job runner) implementation. See `config/config.exs` to see the
5 | # exact configuration with run intervals.
6 | end
7 |
--------------------------------------------------------------------------------
/apps/cf_jobs/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.Jobs.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :cf_jobs,
7 | version: "1.1.0",
8 | build_path: "../../_build",
9 | config_path: "../../config/config.exs",
10 | deps_path: "../../deps",
11 | lockfile: "../../mix.lock",
12 | elixir: "~> 1.6",
13 | elixirc_paths: elixirc_paths(Mix.env()),
14 | build_embedded: Mix.env() == :prod,
15 | start_permanent: Mix.env() == :prod,
16 | aliases: aliases(),
17 | deps: deps(),
18 | test_coverage: [tool: ExCoveralls]
19 | ]
20 | end
21 |
22 | def application do
23 | [
24 | mod: {CF.Jobs.Application, []},
25 | extra_applications: [:logger]
26 | ]
27 | end
28 |
29 | # Specifies which paths to compile per environment.
30 | # Specifies which paths to compile per environment.
31 | defp elixirc_paths(:test), do: ["lib", "test/support"]
32 | defp elixirc_paths(_), do: ["lib"]
33 |
34 | # Dependencies
35 | defp deps do
36 | [
37 | {:quantum, "~> 2.3"},
38 | {:timex, "~> 3.0"},
39 |
40 | # ---- Internal ----
41 | {:cf, in_umbrella: true},
42 | {:db, in_umbrella: true}
43 | ]
44 | end
45 |
46 | defp aliases do
47 | []
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/apps/cf_jobs/test/jobs/create_notifications_test.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.Jobs.CreateNotificationsTest do
2 | use CF.Jobs.DataCase, async: false
3 | alias DB.Schema.UserAction
4 | alias DB.Schema.Notification
5 | alias DB.Schema.UsersActionsReport
6 | alias CF.Jobs.CreateNotifications
7 |
8 | test "creates notifications" do
9 | DB.Repo.delete_all(Notification)
10 | DB.Repo.delete_all(UserAction)
11 | DB.Repo.delete_all(UsersActionsReport)
12 |
13 | subscription = insert(:subscription)
14 | statement = insert(:statement, video: subscription.video)
15 |
16 | action =
17 | insert(
18 | :user_action,
19 | type: :create,
20 | entity: :statement,
21 | video: subscription.video,
22 | statement: statement
23 | )
24 |
25 | CreateNotifications.update(true)
26 |
27 | [notification] = DB.Repo.all(Notification)
28 | assert notification.user_id == subscription.user_id
29 | assert notification.action_id == action.id
30 | assert notification.seen_at == nil
31 | assert notification.type == :new_statement
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/apps/cf_jobs/test/support/data_case.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.Jobs.DataCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | model tests.
5 |
6 | You may define functions here to be used as helpers in
7 | your model tests. See `errors_on/2`'s definition as reference.
8 |
9 | Finally, if the test case interacts with the database,
10 | it cannot be async. For this reason, every test runs
11 | inside a transaction which is reset at the beginning
12 | of the test unless the test case is marked as async.
13 | """
14 |
15 | use ExUnit.CaseTemplate, async: false
16 |
17 | using do
18 | quote do
19 | alias DB.Repo
20 |
21 | import Ecto
22 | import Ecto.Changeset
23 | import Ecto.Query
24 | import CF.Jobs.DataCase
25 | import DB.Factory
26 | end
27 | end
28 |
29 | setup tags do
30 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(DB.Repo)
31 |
32 | unless tags[:async] do
33 | Ecto.Adapters.SQL.Sandbox.mode(DB.Repo, {:shared, self()})
34 | end
35 |
36 | Process.sleep(5)
37 | :ok
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/apps/cf_jobs/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | {:ok, _} = Application.ensure_all_started(:ex_machina)
2 | ExUnit.start()
3 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/config/config.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | config :cf_rest_api,
4 | cors_origins: []
5 |
6 | # Configures the endpoint
7 | config :cf_rest_api, CF.RestApi.Endpoint,
8 | url: [host: "localhost"],
9 | render_errors: [view: CF.RestApi.ErrorView, accepts: ~w(json), default_format: "json"],
10 | pubsub_server: CF.RestApi.PubSub,
11 | server: true
12 |
13 | # Configure Postgres pool size
14 | config :db, DB.Repo, pool_size: 10
15 |
16 | # Import environment specific config
17 | import_config "#{Mix.env()}.exs"
18 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/config/dev.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | dev_secret = "8C6FsJwjV11d+1WPUIbkEH6gB/VavJrcXWoPLujgpclfxjkLkoNFSjVU9XfeNm6s"
4 |
5 | config :cf_rest_api,
6 | cors_origins: "*"
7 |
8 | # For development, we disable any cache and enable
9 | # debugging and code reloading.
10 | config :cf_rest_api, CF.RestApi.Endpoint,
11 | secret_key_base: dev_secret,
12 | debug_errors: false,
13 | code_reloader: false,
14 | check_origin: false,
15 | http: [port: 4000],
16 | force_ssl: false,
17 | https: [
18 | port: 4001,
19 | otp_app: :cf_rest_api,
20 | keyfile: "priv/keys/privkey.pem",
21 | certfile: "priv/keys/fullchain.pem"
22 | ]
23 |
24 | # Set a higher stacktrace during development. Avoid configuring such
25 | # in production as building large stacktraces may be expensive.
26 | config :phoenix, :stacktrace_depth, 20
27 | config :phoenix, :json_library, Jason
28 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/config/prod.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | config :cf_rest_api, CF.RestApi.Endpoint,
4 | force_ssl: false,
5 | check_origin: [],
6 | server: false
7 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/config/test.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | config :cf_rest_api,
4 | cors_origins: "*"
5 |
6 | # We don't run a server during test. If one is required,
7 | # you can enable the server option below.
8 | config :cf_rest_api, CF.RestApi.Endpoint,
9 | http: [port: 10001],
10 | server: false,
11 | force_ssl: false,
12 | secret_key_base: "psZ6n/fq0b444U533yKtve2R0rpjk/IxRGpuanNE92phSDy8/Z2I8lHaIugCMOY7"
13 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/lib/application.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.Application do
2 | use Application
3 |
4 | def start(_type, _args) do
5 | import Supervisor.Spec
6 |
7 | # Define workers and child supervisors to be supervised
8 | children = [
9 | # Start the PubSub system
10 | {Phoenix.PubSub, name: CF.RestApi.PubSub},
11 | # Start the endpoint when the application starts
12 | supervisor(CF.RestApi.Endpoint, []),
13 | # Presence to track number of connected users to a channel
14 | supervisor(CF.RestApi.Presence, [])
15 | ]
16 |
17 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
18 | # for other strategies and supported options
19 | opts = [strategy: :one_for_one, name: CF.RestApi.Supervisor]
20 | Supervisor.start_link(children, opts)
21 | end
22 |
23 | @doc """
24 | Get app's version from `mix.exs`
25 | """
26 | def version() do
27 | case :application.get_key(:cf_rest_api, :vsn) do
28 | {:ok, version} -> to_string(version)
29 | _ -> "unknown"
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/lib/channels/presence.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.Presence do
2 | @moduledoc """
3 | Provides presence tracking to channels and processes.
4 |
5 | See the [`Phoenix.Presence`](http://hexdocs.pm/phoenix/Phoenix.Presence.html)
6 | docs for more details.
7 | """
8 | use Phoenix.Presence, otp_app: :cf_rest_api, pubsub_server: CF.RestApi.PubSub
9 |
10 | def fetch(_topic, entries) do
11 | %{
12 | "viewers" => %{"count" => count_presences(entries, "viewers")},
13 | "users" => %{"count" => count_presences(entries, "users")}
14 | }
15 | end
16 |
17 | defp count_presences(entries, key) do
18 | case get_in(entries, [key, :metas]) do
19 | nil -> 0
20 | metas -> length(metas)
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/lib/controllers/api_info_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.ApiInfoController do
2 | use CF.RestApi, :controller
3 |
4 | def get(conn, _params) do
5 | conn
6 | |> put_status(:ok)
7 | |> json(%{
8 | app: "CF.RestApi",
9 | status: "✔",
10 | version: CF.Application.version(),
11 | db_version: DB.Application.version()
12 | })
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/lib/controllers/fallback_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.FallbackController do
2 | @moduledoc """
3 | Translates controller action results into valid `Plug.Conn` responses.
4 |
5 | See `Phoenix.Controller.action_fallback/1` for more details.
6 | """
7 | use CF.RestApi, :controller
8 |
9 | alias CF.Accounts.UserPermissions.PermissionsError
10 |
11 | def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
12 | conn
13 | |> put_status(:unprocessable_entity)
14 | |> render(CF.RestApi.ChangesetView, "error.json", changeset: changeset)
15 | end
16 |
17 | def call(conn, {:error, :not_found}) do
18 | conn
19 | |> put_status(404)
20 | |> render(CF.RestApi.ErrorView, "error.json", message: "not_found")
21 | end
22 |
23 | def call(conn, {:error, %PermissionsError{}}) do
24 | conn
25 | |> put_status(403)
26 | |> render(CF.RestApi.ErrorView, :"403")
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/lib/controllers/moderation_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.ModerationController do
2 | use CF.RestApi, :controller
3 |
4 | alias CF.Moderation
5 | alias CF.RestApi.ModerationEntryView
6 |
7 | action_fallback(CF.RestApi.FallbackController)
8 |
9 | # All methods here require authentication
10 | plug(Guardian.Plug.EnsureAuthenticated, handler: CF.RestApi.AuthController)
11 |
12 | def random(conn, _) do
13 | case Moderation.random!(Guardian.Plug.current_resource(conn)) do
14 | nil ->
15 | send_resp(conn, 204, "")
16 |
17 | entry ->
18 | render(conn, ModerationEntryView, :show, moderation_entry: entry)
19 | end
20 | end
21 |
22 | def post_feedback(conn, %{"action_id" => id, "value" => value, "reason" => reason})
23 | when is_integer(id) and is_integer(value) and is_integer(reason) do
24 | user = Guardian.Plug.current_resource(conn)
25 | Moderation.feedback!(user, id, value, reason)
26 | send_resp(conn, 204, "")
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/lib/controllers/speaker_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.SpeakerController do
2 | use CF.RestApi, :controller
3 | alias DB.Schema.Speaker
4 |
5 | action_fallback(CF.RestApi.FallbackController)
6 |
7 | def show(conn, %{"slug_or_id" => slug_or_id}) do
8 | case get_speaker(slug_or_id) do
9 | nil ->
10 | conn
11 | |> put_status(:not_found)
12 | |> render(CF.RestApi.ErrorView, "404.json")
13 |
14 | speaker ->
15 | render(conn, "show.json", speaker: speaker)
16 | end
17 | end
18 |
19 | defp get_speaker(slug_or_id) do
20 | case Integer.parse(slug_or_id) do
21 | # It's an ID (string has only number)
22 | {id, ""} ->
23 | Repo.get(Speaker, id)
24 |
25 | # It's a slug (string has at least one alpha character)
26 | _ ->
27 | slug_or_id = Slugger.slugify(slug_or_id)
28 | Repo.get_by(Speaker, slug: slug_or_id)
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/lib/controllers/statement_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.StatementController do
2 | use CF.RestApi, :controller
3 |
4 | alias DB.Schema.Statement
5 | alias DB.Schema.Comment
6 |
7 | def get(conn, %{"video_id" => video_id}) do
8 | video_id = DB.Type.VideoHashId.decode!(video_id)
9 |
10 | statements =
11 | Repo.all(
12 | from(
13 | statement in Statement,
14 | left_join: speaker in assoc(statement, :speaker),
15 | where: statement.video_id == ^video_id,
16 | where: statement.is_removed == false,
17 | order_by: statement.time,
18 | preload: [:speaker, comments: ^Comment.full(Comment, true)]
19 | )
20 | )
21 |
22 | render(conn, "index_full.json", statements: statements)
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/lib/cors.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.CORS do
2 | @spec check_origin(String.t()) :: boolean()
3 | def check_origin(origin) do
4 | case Application.get_env(:cf_rest_api, :cors_origins) do
5 | "*" ->
6 | true
7 |
8 | origins ->
9 | origin in origins
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/lib/endpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.Endpoint do
2 | use Phoenix.Endpoint, otp_app: :cf_rest_api
3 |
4 | socket("/socket", CF.RestApi.UserSocket, websocket: true, longpoll: false)
5 |
6 | if Application.get_env(:arc, :storage) == Arc.Storage.Local,
7 | do: plug(Plug.Static, at: "/resources", from: "./resources", gzip: false)
8 |
9 | plug(Plug.RequestId)
10 | plug(Plug.Logger)
11 | plug(CF.RestApi.SecurityHeaders)
12 |
13 | plug(
14 | Corsica,
15 | max_age: 3600,
16 | allow_headers: ~w(Accept Content-Type Authorization Origin),
17 | origins: {CF.RestApi.CORS, :check_origin}
18 | )
19 |
20 | plug(
21 | Plug.Parsers,
22 | parsers: [:urlencoded, :multipart, :json],
23 | pass: ["*/*"],
24 | json_decoder: Poison
25 | )
26 |
27 | plug(Plug.MethodOverride)
28 | plug(Plug.Head)
29 | plug(CF.RestApi.Router)
30 | end
31 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/lib/security_headers.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.SecurityHeaders do
2 | @x_frame_options if Application.get_env(:cf, :env) == :dev,
3 | do: "SAMEORIGIN",
4 | else: "DENY"
5 |
6 | def init(params), do: params
7 |
8 | def call(conn, _params) do
9 | Plug.Conn.merge_resp_headers(conn, [
10 | {"x-frame-options", @x_frame_options},
11 | {"x-xss-protection", "1; mode=block"},
12 | {"x-content-type-options", "nosniff"},
13 | {"strict-transport-security", "max-age=31536000; includeSubDomains"}
14 | ])
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/lib/views/changeset_view.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.ChangesetView do
2 | use CF.RestApi, :view
3 |
4 | @doc """
5 | Traverses and translates changeset errors.
6 |
7 | See `Ecto.Changeset.traverse_errors/2` and
8 | `CF.RestApi.ErrorHelpers.translate_error/1` for more details.
9 | """
10 | def translate_errors(changeset) do
11 | Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
12 | end
13 |
14 | def render("error.json", %{changeset: changeset}) do
15 | # When encoded, the changeset returns its errors
16 | # as a JSON object. So we just pass it forward.
17 | %{errors: translate_errors(changeset)}
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/lib/views/comment_view.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.CommentView do
2 | use CF.RestApi, :view
3 |
4 | alias CF.RestApi.{CommentView, UserView}
5 |
6 | def render("show.json", %{comment: comment}) do
7 | render_one(comment, CommentView, "comment.json")
8 | end
9 |
10 | def render("index.json", %{comments: comments}) do
11 | render_many(comments, CommentView, "comment.json")
12 | end
13 |
14 | def render("comment.json", %{comment: comment}) do
15 | user =
16 | if Ecto.assoc_loaded?(comment.user) and comment.user_id != nil,
17 | do: UserView.render("show_public.json", %{user: comment.user}),
18 | else: nil
19 |
20 | %{
21 | id: comment.id,
22 | reply_to_id: comment.reply_to_id,
23 | user: user,
24 | statement_id: comment.statement_id,
25 | text: comment.text,
26 | is_reported: comment.is_reported,
27 | approve: comment.approve,
28 | inserted_at: comment.inserted_at,
29 | score: comment.score,
30 | source: render_source(comment.source)
31 | }
32 | end
33 |
34 | defp render_source(nil), do: nil
35 |
36 | defp render_source(source) do
37 | %{
38 | url: source.url,
39 | title: source.title,
40 | language: source.language,
41 | site_name: source.site_name
42 | }
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/lib/views/error_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.ErrorHelpers do
2 | @moduledoc """
3 | Conveniences for translating and building error messages.
4 | """
5 |
6 | use Phoenix.HTML
7 |
8 | @doc """
9 | Generates tag for inlined form input errors.
10 | """
11 | def error_tag(form, field) do
12 | if error = form.errors[field] do
13 | content_tag(:span, translate_error(error), class: "help-block")
14 | end
15 | end
16 |
17 | @doc """
18 | Translates an error message using gettext.
19 | """
20 | def translate_error({msg, opts}) do
21 | # Because error messages were defined within Ecto, we must
22 | # call the Gettext module passing our Gettext backend. We
23 | # also use the "errors" domain as translations are placed
24 | # in the errors.po file.
25 | # Ecto will pass the :count keyword if the error message is
26 | # meant to be pluralized.
27 | # On your own code and templates, depending on whether you
28 | # need the message to be pluralized or not, this could be
29 | # written simply as:
30 | #
31 | # dngettext "errors", "1 file", "%{count} files", count
32 | # dgettext "errors", "is invalid"
33 | #
34 | if count = opts[:count] do
35 | Gettext.dngettext(CF.Gettext, "errors", msg, msg, count, opts)
36 | else
37 | Gettext.dgettext(CF.Gettext, "errors", msg, opts)
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/lib/views/error_view.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.ErrorView do
2 | use CF.RestApi, :view
3 |
4 | require Logger
5 | alias CF.Accounts.UserPermissions.PermissionsError
6 |
7 | def render("show.json", %{message: message}) do
8 | render_one(message, CF.RestApi.ErrorView, "error.json")
9 | end
10 |
11 | def render("401.json", _) do
12 | %{error: "unauthorized"}
13 | end
14 |
15 | def render("403.json", %{reason: %PermissionsError{message: message}}) do
16 | %{error: message}
17 | end
18 |
19 | def render("403.json", _) do
20 | %{error: "forbidden"}
21 | end
22 |
23 | def render("404.json", _) do
24 | %{error: "not_found"}
25 | end
26 |
27 | def render("error.json", %{message: message}) do
28 | %{error: message}
29 | end
30 |
31 | def render("error.json", _) do
32 | %{error: "unexpected"}
33 | end
34 |
35 | def render(_, assigns) do
36 | IO.inspect(assigns)
37 | %{error: "unexpected"}
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/lib/views/flag_view.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.FlagView do
2 | use CF.RestApi, :view
3 |
4 | alias CF.RestApi.UserView
5 |
6 | def render("flag_without_action.json", %{flag: flag}) do
7 | %{
8 | source_user: UserView.render("show.json", user: flag.source_user),
9 | reason: flag.reason
10 | }
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/lib/views/moderation_entry_view.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.ModerationEntryView do
2 | use CF.RestApi, :view
3 |
4 | alias CF.Moderation.ModerationEntry
5 | alias CF.RestApi.UserActionView
6 | alias CF.RestApi.FlagView
7 |
8 | def render("show.json", %{moderation_entry: %ModerationEntry{action: action, flags: flags}}) do
9 | %{
10 | action: render(UserActionView, "user_action.json", %{user_action: action}),
11 | flags: render_many(flags, FlagView, "flag_without_action.json")
12 | }
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/lib/views/speaker_view.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.SpeakerView do
2 | use CF.RestApi, :view
3 |
4 | def render("show.json", %{speaker: speaker}) do
5 | render_one(speaker, CF.RestApi.SpeakerView, "speaker.json")
6 | end
7 |
8 | def render("speaker.json", %{speaker: speaker}) do
9 | %{
10 | id: speaker.id,
11 | slug: speaker.slug,
12 | full_name: speaker.full_name,
13 | title: speaker.title,
14 | picture: DB.Type.SpeakerPicture.full_url(speaker, :thumb),
15 | country: speaker.country,
16 | wikidata_item_id: speaker.wikidata_item_id
17 | }
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/lib/views/statement_view.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.StatementView do
2 | use CF.RestApi, :view
3 |
4 | def render("index.json", %{statements: statements}) do
5 | render_many(statements, CF.RestApi.StatementView, "statement.json")
6 | end
7 |
8 | def render("index_full.json", %{statements: statements}) do
9 | render_many(statements, CF.RestApi.StatementView, "statement_full.json")
10 | end
11 |
12 | def render("show.json", %{statement: statement}) do
13 | render_one(statement, CF.RestApi.StatementView, "statement.json")
14 | end
15 |
16 | def render("statement.json", %{statement: statement}) do
17 | %{
18 | id: statement.id,
19 | text: statement.text,
20 | time: statement.time,
21 | speaker_id: statement.speaker_id,
22 | is_draft: statement.is_draft
23 | }
24 | end
25 |
26 | def render("statement_full.json", %{statement: statement}) do
27 | %{
28 | id: statement.id,
29 | text: statement.text,
30 | time: statement.time,
31 | is_draft: statement.is_draft,
32 | speaker: render_one(statement.speaker, CF.RestApi.SpeakerView, "speaker.json"),
33 | comments: render_many(statement.comments, CF.RestApi.CommentView, "comment.json")
34 | }
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/lib/views/user_action_view.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.UserActionView do
2 | use CF.RestApi, :view
3 |
4 | alias DB.Type.VideoHashId
5 | alias CF.RestApi.UserView
6 | alias CF.RestApi.UserActionView
7 |
8 | def render("index.json", %{users_actions: actions}) do
9 | render_many(actions, UserActionView, "user_action.json")
10 | end
11 |
12 | def render("show.json", %{user_action: action}) do
13 | render_one(action, UserActionView, "user_action.json")
14 | end
15 |
16 | def render("user_action.json", %{user_action: action}) do
17 | %{
18 | id: action.id,
19 | user: UserView.render("show_public.json", %{user: action.user}),
20 | type: action.type,
21 | entity: action.entity,
22 | changes: action.changes,
23 | time: action.inserted_at,
24 | videoId: action.video_id,
25 | videoHashId: action.video_id && VideoHashId.encode(action.video_id),
26 | speakerId: action.speaker_id,
27 | statementId: action.statement_id,
28 | commentId: action.comment_id
29 | }
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/lib/views/user_view.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.UserView do
2 | use CF.RestApi, :view
3 |
4 | alias CF.RestApi.UserView
5 |
6 | def render("index_public.json", %{users: users}) do
7 | render_many(users, UserView, "public_user.json")
8 | end
9 |
10 | def render("show.json", %{user: user}) do
11 | render_one(user, UserView, "user.json")
12 | end
13 |
14 | def render("show_public.json", %{user: user}) do
15 | render_one(user, UserView, "public_user.json")
16 | end
17 |
18 | def render("public_user.json", %{user: user}) do
19 | %{
20 | id: user.id,
21 | name: user.name,
22 | username: user.username,
23 | reputation: user.reputation,
24 | picture_url: DB.Type.UserPicture.full_url(user, :thumb),
25 | mini_picture_url: DB.Type.UserPicture.full_url(user, :mini_thumb),
26 | registered_at: user.inserted_at,
27 | achievements: user.achievements,
28 | speaker_id: user.speaker_id
29 | }
30 | end
31 |
32 | def render("user.json", %{user: user}) do
33 | %{
34 | id: user.id,
35 | email: user.email,
36 | fb_user_id: user.fb_user_id,
37 | name: user.name,
38 | username: user.username,
39 | reputation: user.reputation,
40 | picture_url: DB.Type.UserPicture.full_url(user, :thumb),
41 | mini_picture_url: DB.Type.UserPicture.full_url(user, :mini_thumb),
42 | locale: user.locale,
43 | registered_at: user.inserted_at,
44 | achievements: user.achievements,
45 | is_publisher: user.is_publisher,
46 | speaker_id: user.speaker_id
47 | }
48 | end
49 |
50 | def render("user_with_token.json", %{user: user, token: token}) do
51 | %{
52 | user: UserView.render("show.json", %{user: user}),
53 | token: token
54 | }
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/lib/views/vote_view.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.VoteView do
2 | use CF.RestApi, :view
3 |
4 | def render("my_votes.json", %{votes: votes}) do
5 | render_many(votes, CF.RestApi.VoteView, "my_vote.json")
6 | end
7 |
8 | def render("my_vote.json", %{vote: vote}) do
9 | %{
10 | comment_id: vote.comment_id,
11 | value: vote.value
12 | }
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :cf_rest_api,
7 | version: "1.1.0",
8 | build_path: "../../_build",
9 | compilers: [:phoenix, :gettext] ++ Mix.compilers(),
10 | config_path: "../../config/config.exs",
11 | deps_path: "../../deps",
12 | lockfile: "../../mix.lock",
13 | elixir: "~> 1.6",
14 | elixirc_paths: elixirc_paths(Mix.env()),
15 | build_embedded: Mix.env() == :prod,
16 | start_permanent: Mix.env() == :prod,
17 | aliases: aliases(),
18 | deps: deps(),
19 | test_coverage: [tool: ExCoveralls]
20 | ]
21 | end
22 |
23 | def application do
24 | [
25 | mod: {CF.RestApi.Application, []},
26 | extra_applications: [:logger]
27 | ]
28 | end
29 |
30 | # Specifies which paths to compile per environment.
31 | defp elixirc_paths(:test), do: ["lib", "test/support"]
32 | defp elixirc_paths(:dev), do: ["lib"]
33 | defp elixirc_paths(_), do: ["lib"]
34 |
35 | # Dependencies
36 | defp deps do
37 | [
38 | {:corsica, "~> 2.1"},
39 | {:cowboy, "~> 2.0"},
40 | {:gettext, "~> 0.13.1"},
41 | {:kaur, "~> 1.1"},
42 | {:phoenix, "~> 1.5.14", override: true},
43 | {:phoenix_html, "~> 2.14.3"},
44 | {:phoenix_pubsub, "~> 2.0"},
45 | {:jason, "~> 1.4"},
46 | {:poison, "~> 3.1"},
47 | {:plug_cowboy, "~> 2.7.2"},
48 |
49 | # ---- Internal ----
50 | {:cf, in_umbrella: true},
51 | {:db, in_umbrella: true}
52 | ]
53 | end
54 |
55 | defp aliases do
56 | []
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/priv/keys/fullchain.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIC+zCCAeOgAwIBAgIJAP5GvLkEg+P4MA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV
3 | BAMMCWxvY2FsaG9zdDAeFw0xNzA1MTIwNjEwMzJaFw0yNzA1MTAwNjEwMzJaMBQx
4 | EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
5 | ggEBAL6uymshViiULeyiTXOUYMppzagH4W+bCA75rYyYFUXJJbvvaE3QVvpAyTIz
6 | OXX1rNCvoxxUknV0d3y3JyqN8fU+Oi7Q5rkliZu9tN6xthJrXgRnm12HomNRlC2i
7 | rIXhLTJVcPGM7kbiPZOVKCkN0MF+9EPEvnq9xvtDgzc3ZBKnvUglKrjdwUCZJMTQ
8 | fggtN1PLbNpKGLx3cWBb3SRuzwtVLi34ixMgwnyXzMreH5U1IK7K/hra9vhB1N7j
9 | npx4vlNVagzFZuybuf4Aozne3yioU1z/8sAnHb83DoGs+JnRAnGSZyzg/eGIqdCd
10 | GMTsWH3CHnetE5pNGcwJuL4PFE8CAwEAAaNQME4wHQYDVR0OBBYEFK7VtLoCcnIN
11 | /0YYwfD9iTfTkm3OMB8GA1UdIwQYMBaAFK7VtLoCcnIN/0YYwfD9iTfTkm3OMAwG
12 | A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH/mDv1G3XKdfbzXiPZNiqVa
13 | N17yaffJLFA5Kj33RVwChhutZi0CZYYAuY19BkW9j4+poz2xnRhcvkJqgBdL0mvu
14 | 0NSnCXI/S8+Im97h2aUGgbUQvOxDaeLpLFWt7zZAT9y5zBB4PjwMJY/pp5KAtcNc
15 | GGyfSsr8jhlcriBUqrYUrgX8AvDV8qM2Y++nJ8igmTVjDgWG8hAuipHRmH8r6PBK
16 | 1XeTf4+Q3WB99kEcTglfPgm68KGavRimmuuUejShmulzbNiT+OEMO1KHPIfOc/z9
17 | 4AobuD/k0WNGqOx9Y27A7t7ldKxB7ByXvEwetxyIc3Q9Pv0hrPVGs5ceR1uCk4s=
18 | -----END CERTIFICATE-----
19 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/priv/keys/privkey.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEpAIBAAKCAQEAvq7KayFWKJQt7KJNc5RgymnNqAfhb5sIDvmtjJgVRcklu+9o
3 | TdBW+kDJMjM5dfWs0K+jHFSSdXR3fLcnKo3x9T46LtDmuSWJm7203rG2EmteBGeb
4 | XYeiY1GULaKsheEtMlVw8YzuRuI9k5UoKQ3QwX70Q8S+er3G+0ODNzdkEqe9SCUq
5 | uN3BQJkkxNB+CC03U8ts2koYvHdxYFvdJG7PC1UuLfiLEyDCfJfMyt4flTUgrsr+
6 | Gtr2+EHU3uOenHi+U1VqDMVm7Ju5/gCjOd7fKKhTXP/ywCcdvzcOgaz4mdECcZJn
7 | LOD94Yip0J0YxOxYfcIed60Tmk0ZzAm4vg8UTwIDAQABAoIBACNDf/u/9ocaoEOa
8 | 4Gf3kM7eMkJY8sAJE7xxQD84APce8/OFmuyJEwzE3nCCOKYwAP22/ZtHqK5AE7jk
9 | xkGAbrbEA06VI5Yp8wDyXHiytNFDOefmoTzy0H09oQGvi+hWdF1Sn8iMH6TMQkcA
10 | 1qSBAZJHQDUoNXHNlvbwzVtwyvkH6gXOy1xzPJLWT/9z0TYZN25QJ7hjxVygrqxk
11 | NdDL0J77lBncuMMwTUnUrElJNPEJMaJnINRFs/EpOUZbvFa3sczOfG0JGoJA8Rra
12 | 6jqh4UE5g/ruYR499KaN5zL5Y0w8JeLyglwhGARnEvGl88gpNqBs7KHlEJBhX0jv
13 | 7sEn+kECgYEA+lJdZkC6rNlRiFurJG06AVOL3hej7x1Int/htdVze40OLKlf5Mii
14 | E0Bqp6meRpk8B0IMkhrN8/LN00YV6ktWhRFUBNN0DCCxRVXk1a9fJrvSoDGkVg2Q
15 | Xnd6RjPyW7ub8L5QoUYSuzfcEyULdeWAi7eRDJa0Efaj2AWNYQqQfVsCgYEAwwIZ
16 | MIvEmZkRI/o3aqGoaAcvYJ4+y9TKNhdQCBxsV31YAejzgPLxPaM1+LtoxwZ9Ls8S
17 | gQzskE9bHXylxNMpJApn8DeuIfMLrFa+7VxIC38Gl97C8MAgZvqhSWIpqsY5avBr
18 | D29vYHSidrfioWRM5xmXXO3ys8t/shigANLzcx0CgYEA5LzK2Bsh+byDgmSxmJGu
19 | xXOAhat4g5FwwKy35Z5s7mNQpoMHO1oSsCDW1Opr1PtFHSS/s+qGc/pVFlAeyn+Z
20 | SfMxoU9P5Z0iH8eDWbfs7MoIh5WVI4U1fP0UYH4rYqOmtXBS4WvUxfsfQOdC97KF
21 | qiZNhwFW/mswAL/iFuC+c60CgYBcw/rHpTV4+9+zhawnBY/fLMvU4nJs9GTdJmnj
22 | 8eF4HSBoiDCN/wPTlnhuQnitdODIC6l5ynQekiF9/XW+E9VWV7zqARLNA5lh+kIJ
23 | GAUNsven90g0zrCbTE69Yf0ASBu4S3YieZg6AkHmx8L/k38h0IK4qljyPrQYPK6g
24 | tbkp4QKBgQCKYtF5GvQs2ZCfjYepYuTYzeSOrpy8jnbytlU//KSrzc+htvAuXK70
25 | pwBRilKRlt7h8g1RS+OJi/H8LoOd+sP+lhaEWtHkGGnJTPXfqQ0ETNAeXmUJNP0N
26 | /Auxa38OOGm7owhiKiAIzlE9EwN+7MHcNoaVoBrqT5YtlEPrzAO+/A==
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/test/channels/video_debate_channel_test.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.VideoDebateChannelTest do
2 | use CF.RestApi.ChannelCase
3 |
4 | alias CF.RestApi.VideoDebateChannel
5 |
6 | test "Get video info when connecting" do
7 | video = insert(:video) |> with_video_hash_id()
8 |
9 | {:ok, returned_video, socket} =
10 | subscribe_and_join(
11 | socket(CF.RestApi.UserSocket, "", %{user_id: nil}),
12 | VideoDebateChannel,
13 | "video_debate:#{video.hash_id}"
14 | )
15 |
16 | assert returned_video.id == video.id
17 | assert returned_video.hash_id == video.hash_id
18 | assert returned_video.url == video.url
19 | leave(socket)
20 | end
21 |
22 | test "New speakers get broadcasted" do
23 | # Init
24 | video = insert(:video) |> with_video_hash_id()
25 | topic = "video_debate:#{video.hash_id}"
26 |
27 | {:ok, _, authed_socket} =
28 | subscribe_and_join(
29 | socket(CF.RestApi.UserSocket, "", %{user_id: insert(:user, %{reputation: 5000}).id}),
30 | VideoDebateChannel,
31 | topic
32 | )
33 |
34 | # Test
35 | @endpoint.subscribe("video_debate:#{video.hash_id}")
36 | speaker = %{full_name: "Titi Toto"}
37 | ref = push(authed_socket, "new_speaker", speaker)
38 | assert_reply(ref, :ok, _)
39 | assert_broadcast("speaker_added", %{full_name: "Titi Toto"})
40 |
41 | # Cleanup
42 | leave(authed_socket)
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/test/controllers/api_info_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.ApiInfoControllerTest do
2 | use CF.RestApi.ConnCase
3 |
4 | test "GET / returns API info", %{conn: conn} do
5 | response =
6 | conn
7 | |> get("/")
8 | |> json_response(200)
9 |
10 | assert is_binary(response["version"])
11 | assert response["status"] == "✔"
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/test/support/channel_case.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.ChannelCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | channel tests.
5 |
6 | Such tests rely on `Phoenix.ChannelTest` and also
7 | import other functionality to make it easier
8 | to build and query models.
9 |
10 | Finally, if the test case interacts with the database,
11 | it cannot be async. For this reason, every test runs
12 | inside a transaction which is reset at the beginning
13 | of the test unless the test case is marked as async.
14 | """
15 |
16 | use ExUnit.CaseTemplate
17 | import Mock
18 |
19 | # Mock all calls to Algolia
20 | setup_with_mocks([
21 | {Algoliax.Client, [], [request: fn _ -> nil end]}
22 | ]) do
23 | :ok
24 | end
25 |
26 | using do
27 | quote do
28 | # Import conveniences for testing with channels
29 | use Phoenix.ChannelTest
30 | import DB.Factory
31 |
32 | # The default endpoint for testing
33 | @endpoint CF.RestApi.Endpoint
34 | end
35 | end
36 |
37 | setup tags do
38 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(DB.Repo)
39 |
40 | unless tags[:async] do
41 | Ecto.Adapters.SQL.Sandbox.mode(DB.Repo, {:shared, self()})
42 | end
43 |
44 | :ok
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/test/support/conn_case.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.RestApi.ConnCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | tests that require setting up a connection.
5 |
6 | Finally, if the test case interacts with the database,
7 | it cannot be async. For this reason, every test runs
8 | inside a transaction which is reset at the beginning
9 | of the test unless the test case is marked as async.
10 | """
11 |
12 | use ExUnit.CaseTemplate
13 |
14 | using do
15 | quote do
16 | # Import conveniences for testing with connections
17 | import Plug.Conn
18 | import Phoenix.ConnTest
19 | import CF.RestApi.Router.Helpers
20 |
21 | # The default endpoint for testing
22 | @endpoint CF.RestApi.Endpoint
23 |
24 | alias CF.Authenticator.GuardianImpl
25 | alias DB.Repo
26 |
27 | def build_authenticated_conn(user) do
28 | {:ok, token, _} = GuardianImpl.encode_and_sign(user)
29 |
30 | Phoenix.ConnTest.build_conn()
31 | |> Plug.Conn.put_req_header("authorization", "Bearer #{token}")
32 | end
33 | end
34 | end
35 |
36 | setup tags do
37 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(DB.Repo)
38 |
39 | unless tags[:async] do
40 | Ecto.Adapters.SQL.Sandbox.mode(DB.Repo, {:shared, self()})
41 | end
42 |
43 | {:ok, conn: Phoenix.ConnTest.build_conn()}
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | {:ok, _} = Application.ensure_all_started(:ex_machina)
2 | ExUnit.start()
3 |
--------------------------------------------------------------------------------
/apps/cf_rest_api/test/views/error_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.ErrorViewTest do
2 | use CF.RestApi.ConnCase
3 |
4 | # Bring render/3 and render_to_string/3 for testing custom views
5 | import Phoenix.View
6 | alias CF.RestApi.ErrorView
7 | alias CF.Accounts.UserPermissions.PermissionsError
8 |
9 | test "renders 401.json" do
10 | assert render_to_string(ErrorView, "401.json", []) =~ "unauthorized"
11 | end
12 |
13 | test "renders 403.json" do
14 | assert render_to_string(ErrorView, "403.json", []) =~ "forbidden"
15 | end
16 |
17 | test "renders 403.json with PermissionsError" do
18 | assert render_to_string(ErrorView, "403.json", %{reason: %PermissionsError{message: "xxx"}}) =~
19 | "xxx"
20 | end
21 |
22 | test "renders 404.json" do
23 | assert render_to_string(ErrorView, "404.json", []) =~ "not_found"
24 | end
25 |
26 | test "render any other" do
27 | assert render_to_string(ErrorView, "999.json", []) =~ "unexpected"
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/apps/cf_reverse_proxy/config/config.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # Configures the endpoint
4 | config :cf_reverse_proxy, port: 5000
5 |
6 | # Import environment specific config
7 | import_config "#{Mix.env()}.exs"
8 |
--------------------------------------------------------------------------------
/apps/cf_reverse_proxy/config/dev.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
--------------------------------------------------------------------------------
/apps/cf_reverse_proxy/config/prod.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | config :cf_reverse_proxy, port: 80
4 |
--------------------------------------------------------------------------------
/apps/cf_reverse_proxy/config/test.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
--------------------------------------------------------------------------------
/apps/cf_reverse_proxy/lib/application.ex:
--------------------------------------------------------------------------------
1 | defmodule CF.ReverseProxy.Application do
2 | use Application
3 |
4 | require Logger
5 |
6 | def start(_type, _args) do
7 | import Supervisor.Spec, warn: false
8 | port = Application.get_env(:cf_reverse_proxy, :port)
9 |
10 | cowboy =
11 | {Plug.Cowboy,
12 | scheme: :http,
13 | plug: CF.ReverseProxy.Plug,
14 | port: port,
15 | dispatch: [
16 | {:_,
17 | [
18 | {"/socket/websocket", Phoenix.Endpoint.Cowboy2Handler, {CF.RestApi.Endpoint, []}},
19 | {"/socket/longpoll", Phoenix.Endpoint.Cowboy2Handler, {CF.RestApi.Endpoint, []}},
20 | {:_, Plug.Cowboy.Handler, {CF.ReverseProxy.Plug, []}}
21 | ]}
22 | ]}
23 |
24 | Logger.info("Running CF.ReverseProxy with cowboy on port #{port}")
25 | opts = [strategy: :one_for_one, name: CF.ReverseProxy.Supervisor]
26 | Supervisor.start_link([cowboy], opts)
27 | end
28 |
29 | def config_change(_changed, _new, _removed) do
30 | :ok
31 | end
32 |
33 | def version() do
34 | case :application.get_key(:cf_reverse_proxy, :vsn) do
35 | {:ok, version} -> to_string(version)
36 | _ -> "unknown"
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/apps/cf_reverse_proxy/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.ReverseProxy.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :cf_reverse_proxy,
7 | version: "1.0.0",
8 | build_path: "../../_build",
9 | compilers: [:phoenix] ++ Mix.compilers(),
10 | config_path: "../../config/config.exs",
11 | deps_path: "../../deps",
12 | lockfile: "../../mix.lock",
13 | elixir: "~> 1.6",
14 | elixirc_paths: elixirc_paths(Mix.env()),
15 | build_embedded: Mix.env() == :prod,
16 | start_permanent: Mix.env() == :prod,
17 | aliases: aliases(),
18 | deps: deps()
19 | ]
20 | end
21 |
22 | def application do
23 | [
24 | mod: {CF.ReverseProxy.Application, []},
25 | extra_applications: [:logger]
26 | ]
27 | end
28 |
29 | # Specifies which paths to compile per environment.
30 | defp elixirc_paths(:test), do: ["lib", "test/support"]
31 | defp elixirc_paths(:dev), do: ["lib"]
32 | defp elixirc_paths(_), do: ["lib"]
33 |
34 | # Dependencies
35 | defp deps do
36 | [
37 | {:cf_rest_api, in_umbrella: true},
38 | {:cf_graphql, in_umbrella: true},
39 | {:cf_atom_feed, in_umbrella: true},
40 | {:phoenix, "~> 1.5.14"},
41 | {:jason, "~> 1.4"},
42 | {:cowboy, "~> 2.0"},
43 | {:corsica, "~> 2.1"}
44 | ]
45 | end
46 |
47 | defp aliases do
48 | []
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/apps/db/README.md:
--------------------------------------------------------------------------------
1 | # [CaptainFact App] Database
2 |
3 | This is CaptainFact database schemas and types.
4 |
5 | ## Secrets
6 |
7 | Following secrets must be configured in production:
8 |
9 | - db_hostname
10 | - db_username
11 | - db_password
12 | - db_name
13 |
--------------------------------------------------------------------------------
/apps/db/config/config.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # General application configuration
4 | config :db,
5 | env: Mix.env(),
6 | ecto_repos: [DB.Repo]
7 |
8 | # Database: use postgres
9 | config :db, DB.Repo,
10 | adapter: Ecto.Adapters.Postgres,
11 | pool_size: 3,
12 | loggers: [
13 | {Ecto.LogEntry, :log, []}
14 | ]
15 |
16 | # Import environment specific config
17 | import_config "#{Mix.env()}.exs"
18 |
--------------------------------------------------------------------------------
/apps/db/config/dev.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # Configure your database
4 | config :db, DB.Repo,
5 | username: "postgres",
6 | password: "postgres",
7 | database: "captain_fact_dev",
8 | hostname: "localhost"
9 |
10 | # Configure file upload
11 | config :arc, storage: Arc.Storage.Local, asset_host: "http://localhost:4000"
12 |
--------------------------------------------------------------------------------
/apps/db/config/prod.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # Do not print debug messages in production
4 | config :logger, level: :info
5 |
--------------------------------------------------------------------------------
/apps/db/config/test.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # Print only warnings and errors during test
4 | config :logger, level: :warn
5 |
6 | # Configure file upload
7 | config :arc, storage: Arc.Storage.Local
8 |
9 | # Configure your database
10 | config :db, DB.Repo,
11 | hostname:
12 | if(
13 | is_nil(System.get_env("CF_DB_HOSTNAME")),
14 | do: "localhost",
15 | else: System.get_env("CF_DB_HOSTNAME")
16 | ),
17 | username: "postgres",
18 | password: "postgres",
19 | database: "captain_fact_test",
20 | pool: Ecto.Adapters.SQL.Sandbox
21 |
--------------------------------------------------------------------------------
/apps/db/lib/db/application.ex:
--------------------------------------------------------------------------------
1 | defmodule DB.Application do
2 | @moduledoc false
3 |
4 | use Application
5 | require Logger
6 |
7 | def start(_type, _args) do
8 | import Supervisor.Spec, warn: false
9 |
10 | # Define workers and child supervisors to be supervised
11 | children = [
12 | # Starts a worker by calling: DB.Worker.start_link(arg1, arg2, arg3)
13 | # worker(DB.Worker, [arg1, arg2, arg3]),
14 | supervisor(DB.Repo, [])
15 | ]
16 |
17 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
18 | # for other strategies and supported options
19 | opts = [strategy: :one_for_one, name: DB.Supervisor]
20 | link = Supervisor.start_link(children, opts)
21 | migrate_db()
22 | link
23 | end
24 |
25 | defp migrate_db do
26 | Logger.info("Running migrations...")
27 | Ecto.Migrator.run(DB.Repo, migrations_path(), :up, all: true)
28 | Logger.info("Migrated!")
29 | end
30 |
31 | defp migrations_path do
32 | Path.join([:code.priv_dir(:db), "repo", "migrations"])
33 | end
34 |
35 | def version() do
36 | case :application.get_key(:db, :vsn) do
37 | {:ok, version} -> to_string(version)
38 | _ -> "unknown"
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/apps/db/lib/db/repo.ex:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo do
2 | use Ecto.Repo, otp_app: :db, adapter: Ecto.Adapters.Postgres
3 | use Scrivener, page_size: 10
4 | end
5 |
--------------------------------------------------------------------------------
/apps/db/lib/db/statistics.ex:
--------------------------------------------------------------------------------
1 | defmodule DB.Statistics do
2 | import Ecto.Query
3 |
4 | alias DB.Schema.User
5 | alias DB.Repo
6 |
7 | @doc """
8 | A shortcut to returns the amount of user in the database
9 | """
10 | @spec all_totals() :: %{
11 | users: integer,
12 | comments: integer,
13 | statements: integer,
14 | sources: integer
15 | }
16 | def all_totals() do
17 | Repo
18 | |> Ecto.Adapters.SQL.query!("""
19 | SELECT (select count(id) FROM users) as users,
20 | (select count(id) FROM comments) as comments,
21 | (select count(id) FROM statements) as statements,
22 | (select count(id) FROM sources) as sources
23 | """)
24 | |> (fn %Postgrex.Result{rows: [[users, comments, statements, sources]]} ->
25 | %{users: users, comments: comments, statements: statements, sources: sources}
26 | end).()
27 | end
28 |
29 | @doc """
30 | returns the 20 most active users
31 | """
32 | @spec leaderboard() :: list(%User{})
33 | def leaderboard do
34 | from(
35 | u in User,
36 | order_by: [desc: u.reputation],
37 | limit: 20
38 | )
39 | |> Repo.all()
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/apps/db/lib/db_schema/flag.ex:
--------------------------------------------------------------------------------
1 | defmodule DB.Schema.Flag do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 |
5 | alias DB.Schema.{User, UserAction}
6 |
7 | schema "flags" do
8 | # Source user
9 | belongs_to(:source_user, User)
10 | belongs_to(:action, UserAction)
11 | field(:reason, DB.Type.FlagReason)
12 | timestamps()
13 | end
14 |
15 | @required_fields ~w(source_user_id action_id reason)a
16 |
17 | @doc """
18 | Builds a changeset based on an `UserAction`
19 | """
20 | def changeset(struct, params) do
21 | struct
22 | |> cast(params, [:action_id, :reason])
23 | |> validate_required(@required_fields)
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/apps/db/lib/db_schema/invitation_request.ex:
--------------------------------------------------------------------------------
1 | defmodule DB.Schema.InvitationRequest do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 |
5 | alias DB.Schema.User
6 | alias DB.Schema.InvitationRequest
7 |
8 | schema "invitation_requests" do
9 | field(:email, :string)
10 | field(:invitation_sent, :boolean, default: false)
11 | field(:token, :string)
12 | field(:locale, :string)
13 |
14 | belongs_to(:invited_by, User)
15 |
16 | timestamps()
17 | end
18 |
19 | @doc false
20 | def changeset(invitation_request = %InvitationRequest{}, attrs) do
21 | invitation_request
22 | |> cast(attrs, [:email, :invited_by_id, :locale])
23 | |> validate_required([:email])
24 | |> User.validate_email()
25 | |> User.validate_locale()
26 | |> unique_constraint(:email)
27 | end
28 |
29 | def changeset_token(request, token) do
30 | change(request, token: token)
31 | end
32 |
33 | def changeset_sent(request, is_sent) do
34 | change(request, invitation_sent: is_sent)
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/apps/db/lib/db_schema/moderation_user_feedback.ex:
--------------------------------------------------------------------------------
1 | defmodule DB.Schema.ModerationUserFeedback do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 | alias DB.Schema.ModerationUserFeedback
5 |
6 | schema "moderation_users_feedbacks" do
7 | field(:value, :integer)
8 | field(:user_id, :id)
9 | field(:action_id, :id)
10 | field(:flag_reason, DB.Type.FlagReason)
11 |
12 | timestamps()
13 | end
14 |
15 | @doc false
16 | def changeset(user_feedback = %ModerationUserFeedback{}, attrs) do
17 | user_feedback
18 | |> cast(attrs, [:value, :flag_reason])
19 | |> validate_required([:value, :action_id, :user_id, :flag_reason])
20 | |> validate_number(:value, greater_than_or_equal_to: -1, less_than_or_equal_to: 1)
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/apps/db/lib/db_schema/notification.ex:
--------------------------------------------------------------------------------
1 | defmodule DB.Schema.Notification do
2 | @moduledoc """
3 | Represent a user's Notification.
4 | """
5 |
6 | use Ecto.Schema
7 | import Ecto.Changeset
8 |
9 | schema "notifications" do
10 | belongs_to(:user, DB.Schema.User)
11 | belongs_to(:action, DB.Schema.UserAction)
12 |
13 | field(:type, DB.Type.NotificationType)
14 | field(:seen_at, :utc_datetime, default: nil)
15 |
16 | timestamps()
17 | end
18 |
19 | @fields [:user_id, :action_id, :type, :seen_at]
20 | @required_fields [:user_id, :action_id, :type]
21 |
22 | @doc """
23 | Builds a changeset based on the `struct` and `params`.
24 | """
25 | def changeset(struct, params \\ %{}) do
26 | struct
27 | |> cast(params, @fields)
28 | |> validate_required(@required_fields)
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/apps/db/lib/db_schema/reset_password_request.ex:
--------------------------------------------------------------------------------
1 | defmodule DB.Schema.ResetPasswordRequest do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 |
5 | @primary_key {:token, :string, []}
6 | schema "accounts_reset_password_requests" do
7 | field(:source_ip, :string)
8 | belongs_to(:user, DB.Schema.User)
9 |
10 | timestamps(updated_at: false)
11 | end
12 |
13 | @token_length 128
14 |
15 | def changeset(model, attrs) do
16 | model
17 | |> cast(attrs, [:source_ip, :user_id])
18 | |> change(token: DB.Utils.TokenGenerator.generate(@token_length))
19 | |> validate_required([:source_ip, :user_id, :token])
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/apps/db/lib/db_schema/users_actions_report.ex:
--------------------------------------------------------------------------------
1 | defmodule DB.Schema.UsersActionsReport do
2 | @moduledoc """
3 | A report generated by a job to provide status and statistics.
4 | """
5 |
6 | use Ecto.Schema
7 | import Ecto.Changeset
8 | alias DB.Schema.UsersActionsReport
9 |
10 | schema "users_actions_reports" do
11 | field(:analyser_id, :integer)
12 | field(:last_action_id, :integer)
13 | field(:status, :integer)
14 |
15 | # Various stats
16 | field(:nb_actions, :integer)
17 | field(:nb_entries_updated, :integer)
18 | field(:run_duration, :integer)
19 |
20 | timestamps()
21 | end
22 |
23 | @required [:analyser_id, :status, :last_action_id, :nb_actions]
24 |
25 | @doc false
26 | def changeset(report = %UsersActionsReport{}, attrs) do
27 | report
28 | |> cast(attrs, @required)
29 | |> validate_required(@required)
30 | end
31 |
32 | def analyser_id(:reputation), do: 1
33 | def analyser_id(:flags), do: 2
34 | def analyser_id(:achievements), do: 3
35 | def analyser_id(:votes), do: 4
36 | def analyser_id(:create_notifications), do: 5
37 | def analyser_id(:download_captions), do: 6
38 |
39 | def status(:pending), do: 1
40 | def status(:running), do: 2
41 | def status(:success), do: 3
42 | def status(:failed), do: 4
43 | end
44 |
--------------------------------------------------------------------------------
/apps/db/lib/db_schema/video_caption.ex:
--------------------------------------------------------------------------------
1 | defmodule DB.Schema.VideoCaption do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 |
5 | @primary_key false
6 | schema "videos_captions" do
7 | belongs_to(:video, DB.Schema.Video, primary_key: true)
8 | field(:raw, :string)
9 | field(:parsed, {:array, :map})
10 | field(:format, :string)
11 |
12 | timestamps()
13 | end
14 |
15 | @required_fields ~w(video_id raw parsed format)a
16 |
17 | @doc """
18 | Builds a changeset based on the `struct` and `params`.
19 | """
20 | def changeset(struct, params \\ %{}) do
21 | struct
22 | |> cast(params, @required_fields)
23 | |> validate_required(@required_fields)
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/apps/db/lib/db_schema/video_speaker.ex:
--------------------------------------------------------------------------------
1 | defmodule DB.Schema.VideoSpeaker do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 |
5 | @primary_key false
6 | schema "videos_speakers" do
7 | belongs_to(:video, DB.Schema.Video, primary_key: true)
8 | belongs_to(:speaker, DB.Schema.Speaker, primary_key: true)
9 |
10 | timestamps()
11 | end
12 |
13 | @required_fields ~w(video_id speaker_id)a
14 |
15 | @doc """
16 | Builds a changeset based on the `struct` and `params`.
17 | """
18 | def changeset(struct, params \\ %{}) do
19 | struct
20 | |> cast(params, @required_fields)
21 | |> validate_required(@required_fields)
22 | |> unique_constraint(:video, name: :videos_speakers_pkey)
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/apps/db/lib/db_type/achievement.ex:
--------------------------------------------------------------------------------
1 | defmodule DB.Type.Achievement do
2 | @moduledoc """
3 | Translate a user achievement from atom to integer.
4 | TODO: Migrate this to a real Ecto.Type using Ecto.Enum
5 | """
6 |
7 | @doc """
8 | Get an achievement id from an easy to use / remember atom.
9 | You can add more at the end, but you CANNOT change
10 | existing identifiers, it would break existing achievements
11 | """
12 | # Default badge
13 | def get(:welcome), do: 1
14 | # Validate email or link third party account
15 | def get(:not_a_robot), do: 2
16 | # Visit help pages
17 | def get(:help), do: 3
18 | # Install extension
19 | def get(:bulletproof), do: 4
20 | # ???
21 | def get(:you_are_fake_news), do: 5
22 | # Link third party account
23 | def get(:social_networks), do: 6
24 | # Ambassador
25 | def get(:ambassador), do: 7
26 | # Made a bug report
27 | def get(:ghostbuster), do: 8
28 | # Leaderboard
29 | def get(:famous), do: 9
30 | # Made a contribution on the graphics
31 | def get(:artist), do: 10
32 | # Made a suggestion that gets approved
33 | def get(:good_vibes), do: 11
34 | end
35 |
--------------------------------------------------------------------------------
/apps/db/lib/db_type/entity.ex:
--------------------------------------------------------------------------------
1 | import EctoEnum
2 |
3 | defenum(
4 | DB.Type.Entity,
5 | video: 1,
6 | speaker: 2,
7 | statement: 3,
8 | comment: 4,
9 | fact: 5,
10 | user_action: 6,
11 | user: 7,
12 | video_caption: 8
13 | )
14 |
--------------------------------------------------------------------------------
/apps/db/lib/db_type/notification_type.ex:
--------------------------------------------------------------------------------
1 | import EctoEnum
2 |
3 | defenum(
4 | DB.Type.NotificationType,
5 | :notification_type,
6 | [
7 | # Default notification type, for when type is unknown
8 | :default,
9 | # Notifications generated by `create_notifications` job
10 | :reply_to_comment,
11 | :new_comment,
12 | :new_statement,
13 | :new_speaker,
14 | :updated_statement,
15 | :updated_video,
16 | :updated_speaker,
17 | :removed_statement,
18 | :removed_speaker,
19 | # Notifications below are meant to be dispatched manualy
20 | :new_achievement,
21 | :email_confirmed
22 | ]
23 | )
24 |
--------------------------------------------------------------------------------
/apps/db/lib/db_type/speaker_picture.ex:
--------------------------------------------------------------------------------
1 | defmodule DB.Type.SpeakerPicture do
2 | @moduledoc """
3 | Speaker picture. Map the Ecto type to an URL using ARC
4 | """
5 |
6 | use Arc.Definition
7 | use Arc.Ecto.Definition
8 |
9 | @versions [:thumb]
10 | @extension_whitelist ~w(.jpg .jpeg .png)
11 |
12 | # Whitelist file extensions:
13 | def validate({file, _}) do
14 | file_extension = file.file_name |> Path.extname() |> String.downcase()
15 | Enum.member?(@extension_whitelist, file_extension)
16 | end
17 |
18 | # The default `url` function has a bug where it does not include the host
19 | def full_url(speaker, version) do
20 | path = url({speaker.picture, speaker}, version)
21 |
22 | cond do
23 | is_nil(path) -> nil
24 | String.starts_with?(path, "/") -> "#{Application.get_env(:arc, :asset_host)}/#{path}"
25 | true -> path
26 | end
27 | end
28 |
29 | # Define a thumbnail transformation:
30 | def transform(:thumb, _) do
31 | {:convert, "-thumbnail 50x50^ -gravity center -extent 50x50 -format jpg", :jpg}
32 | end
33 |
34 | # Override the persisted filenames:
35 | def filename(version, {_, speaker}) do
36 | "#{speaker.id}_#{version}"
37 | end
38 |
39 | # Override the storage directory:
40 | def storage_dir(_, {_, _}) do
41 | "resources/speakers"
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/apps/db/lib/db_type/subscription_reason.ex:
--------------------------------------------------------------------------------
1 | import EctoEnum
2 |
3 | defenum(
4 | DB.Type.SubscriptionReason,
5 | :subscription_reason,
6 | [
7 | :is_author,
8 | :manual,
9 | :suggestion
10 | ]
11 | )
12 |
--------------------------------------------------------------------------------
/apps/db/lib/db_type/user_action_type.ex:
--------------------------------------------------------------------------------
1 | import EctoEnum
2 |
3 | defenum(
4 | DB.Type.UserActionType,
5 | # Common
6 | create: 1,
7 | remove: 2,
8 | update: 3,
9 | delete: 4,
10 | add: 5,
11 | restore: 6,
12 | approve: 7,
13 | flag: 8,
14 | # Voting stuff
15 | vote_up: 9,
16 | vote_down: 10,
17 | self_vote: 11,
18 | revert_vote_up: 12,
19 | revert_vote_down: 13,
20 | revert_self_vote: 14,
21 | # Bans - See DB.Type.FlagReason for labels
22 | action_banned_bad_language: 21,
23 | action_banned_spam: 22,
24 | action_banned_irrelevant: 23,
25 | action_banned_not_constructive: 24,
26 | # Special actions
27 | email_confirmed: 100,
28 | collective_moderation: 101,
29 | start_automatic_statements_extraction: 102,
30 | upload: 110,
31 | # Flags
32 | abused_flag: 103,
33 | confirmed_flag: 104,
34 | # Deprecated
35 | action_banned: 102,
36 | social_network_linked: 105
37 | )
38 |
--------------------------------------------------------------------------------
/apps/db/lib/db_utils/string.ex:
--------------------------------------------------------------------------------
1 | defmodule DB.Utils.String do
2 | @moduledoc """
3 | String utils not included in base library
4 | """
5 |
6 | @doc """
7 | Convert a string like " aaa bbb ccc " into "aaa bbb ccc"
8 |
9 | ## Examples
10 |
11 | iex> DB.Utils.String.trim_all_whitespaces " aaa bbb ccc "
12 | "aaa bbb ccc"
13 | iex> DB.Utils.String.trim_all_whitespaces ""
14 | ""
15 | """
16 | def trim_all_whitespaces(nil),
17 | do: nil
18 |
19 | def trim_all_whitespaces(str) do
20 | str
21 | |> String.trim()
22 | |> String.replace(~r/\s+/, " ")
23 | end
24 |
25 | def upcase(nil), do: nil
26 |
27 | def upcase(str), do: String.upcase(str)
28 | end
29 |
--------------------------------------------------------------------------------
/apps/db/lib/db_utils/token_generator.ex:
--------------------------------------------------------------------------------
1 | defmodule DB.Utils.TokenGenerator do
2 | @moduledoc """
3 | Generate base64 unique tokens using :crypto.strong_rand_bytes/1
4 | """
5 |
6 | @doc """
7 | Generate a new token
8 | """
9 | def generate(length) do
10 | length
11 | |> :crypto.strong_rand_bytes()
12 | |> Base.url_encode64()
13 | |> binary_part(0, length)
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/apps/db/lib/query/query.ex:
--------------------------------------------------------------------------------
1 | defmodule DB.Query do
2 | @moduledoc """
3 | General queriying utils.
4 | """
5 |
6 | import Ecto.Query
7 |
8 | @doc """
9 | Revert sort by last_inserted. Fallsback on `id` in case there's an equality.
10 | """
11 | @spec order_by_last_inserted_desc(Ecto.Queryable.t()) :: Ecto.Queryable.t()
12 | def order_by_last_inserted_desc(query) do
13 | order_by(query, desc: :inserted_at, desc: :id)
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20170118223600_create_postgres_extensions.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.CreatePostgresExtensions do
2 | use Ecto.Migration
3 |
4 | def change do
5 | execute("CREATE EXTENSION IF NOT EXISTS citext;")
6 | execute("CREATE EXTENSION IF NOT EXISTS unaccent;")
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20170118223631_create_users.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.CreateUsers do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:users) do
6 | add :username, :citext, null: false
7 | add :email, :citext, null: false
8 | add :encrypted_password, :string, null: false
9 | add :name, :citext, null: true
10 | add :picture_url, :string, null: true
11 | add :reputation, :integer, null: false, default: 0
12 | add :locale, :string, null: true
13 |
14 | # Social networks profiles
15 | add :fb_user_id, :string, null: true
16 |
17 | # Email confirmation
18 | add :email_confirmed, :boolean, null: false, default: false
19 | add :email_confirmation_token, :string, null: true
20 |
21 | timestamps()
22 | end
23 |
24 | create unique_index(:users, [:email])
25 | create unique_index(:users, [:username])
26 | create unique_index(:users, [:fb_user_id])
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20170125235612_create_videos.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.CreateVideo do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:videos) do
6 | add :provider, :string, null: false
7 | add :provider_id, :string, null: false
8 | add :title, :string, null: false
9 |
10 | timestamps()
11 | end
12 | create unique_index(:videos, [:provider, :provider_id])
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20170206062334_create_speakers.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.CreateSpeaker do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:speakers) do
6 | add :full_name, :citext, null: false
7 | add :title, :string
8 | add :is_user_defined, :boolean, null: false
9 | add :picture, :string
10 | add :wikidata_item_id, :integer
11 | add :country, :string
12 | add :is_removed, :boolean, null: false, default: false
13 |
14 | timestamps()
15 | end
16 | create index(:speakers, :full_name, where: "is_user_defined = false")
17 | create unique_index(:speakers, :wikidata_item_id, where: "is_user_defined = false")
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20170206063137_create_statements.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.CreateStatement do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:statements) do
6 | add :text, :string, null: false
7 | add :time, :integer, null: false
8 | add :is_removed, :boolean, null: false, default: false
9 |
10 | add :video_id, references(:videos, on_delete: :delete_all), null: false
11 | add :speaker_id, references(:speakers, on_delete: :nilify_all), null: true
12 |
13 | timestamps()
14 | end
15 | create index(:statements, [:video_id], where: "is_removed = false")
16 | create index(:statements, [:speaker_id], where: "is_removed = false")
17 |
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20170221035619_create_video_speaker.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.CreateVideoSpeaker do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:videos_speakers, primary_key: false) do
6 | add :video_id, references(:videos, on_delete: :delete_all), primary_key: true
7 | add :speaker_id, references(:speakers, on_delete: :delete_all), primary_key: true
8 |
9 | timestamps()
10 | end
11 | create unique_index(:videos_speakers, [:video_id, :speaker_id], name: :videos_speakers_index)
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20170309214200_create_source.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.CreateSource do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:sources) do
6 | add :url, :string, null: false
7 | add :title, :string, null: true
8 | add :language, :string, null: true
9 | add :site_name, :string, null: true
10 |
11 | timestamps()
12 | end
13 | create unique_index(:sources, :url)
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20170309214307_create_comment.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.CreateComment do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:comments) do
6 | add :user_id, references(:users, on_delete: :delete_all), null: false
7 | add :statement_id, references(:statements, on_delete: :delete_all), null: false
8 | add :source_id, references(:sources, on_delete: :nilify_all), null: true
9 | add :reply_to_id, references(:comments, on_delete: :delete_all), null: true
10 |
11 | add :text, :string
12 | add :approve, :boolean
13 | add :is_banned, :boolean, null: false, default: false
14 |
15 | timestamps()
16 | end
17 | create index(:comments, [:user_id])
18 | create index(:comments, [:statement_id], where: "is_banned = false")
19 |
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20170316233954_create_vote.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.CreateVote do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:votes, primary_key: false) do
6 | add :value, :integer, null: false
7 |
8 | add :user_id, references(:users, on_delete: :delete_all), primary_key: true
9 | add :comment_id, references(:comments, on_delete: :delete_all), primary_key: true
10 |
11 | timestamps()
12 | end
13 | create unique_index(:votes, [:user_id, :comment_id], name: "user_comment_index")
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20170428062411_create_video_debate_action.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.CreateVideoDebateAction do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:video_debate_actions) do
6 | add :user_id, references(:users, on_delete: :nothing), null: true
7 | add :video_id, references(:videos, on_delete: :nothing), null: false
8 | add :entity, :string, null: false
9 | add :entity_id, :integer, null: false
10 | add :type, :string, null: false
11 | add :changes, :map, null: true
12 |
13 | timestamps(updated_at: false)
14 | end
15 | create index(:video_debate_actions, [:user_id])
16 | create index(:video_debate_actions, [:video_id])
17 | create index(:video_debate_actions, [:entity, :entity_id])
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20170611075306_create_flag.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.CreateFlag do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:flags) do
6 | add :type, :integer, null: false
7 | add :reason, :integer, null: false
8 | add :entity_id, :integer, null: false
9 |
10 | add :source_user_id, references(:users, on_delete: :delete_all), null: false
11 | add :target_user_id, references(:users, on_delete: :delete_all), null: false
12 |
13 | timestamps()
14 | end
15 | create index(:flags, [:source_user_id])
16 | create index(:flags, [:target_user_id])
17 | create unique_index(:flags, [:source_user_id, :type, :entity_id])
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20170726224741_create_accounts_reset_password_request.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.CreateDB.Accounts.ResetPasswordRequest do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:accounts_reset_password_requests, primary_key: false) do
6 | add :token, :string, primary_key: true, null: false
7 | add :source_ip, :string, null: false
8 | add :user_id, references(:users, on_delete: :delete_all), null: false
9 |
10 | timestamps(updated_at: false)
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20170730064848_create_invitation_requests.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.CreateInvitationRequests do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:invitation_requests) do
6 | add :email, :string, null: true
7 | add :token, :string, null: true
8 | add :invitation_sent, :boolean, default: false, null: false
9 | add :invited_by_id, references(:users, on_delete: :nilify_all), null: true
10 |
11 | timestamps()
12 | end
13 |
14 | create unique_index(:invitation_requests, [:email])
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20170928043353_add_language_to_videos.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.AddLanguageToVideos do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:videos) do
6 | add :language, :string, size: 2
7 | end
8 | create index(:videos, :language)
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20171003220327_create_achievements.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.CreateAchievements do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:achievements) do
6 | add :slug, :string
7 | add :rarity, :integer
8 |
9 | timestamps()
10 | end
11 |
12 | create unique_index(:achievements, [:slug])
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20171003220416_add_achievements_to_users.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.AddAchievementsToUsers do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:users) do
6 | add :achievements, {:array, :integer}, default: [], null: false
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20171004100258_create_users_actions.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.CreateUsersActions do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:users_actions) do
6 | add :user_id, references(:users, on_delete: :nothing), null: false
7 | add :target_user_id, references(:users, on_delete: :nothing), null: true
8 |
9 | add :type, :integer, null: false
10 | add :entity, :integer, null: false
11 | add :context, :string, null: true
12 | add :entity_id, :integer, null: true
13 | add :changes, :map, null: true
14 |
15 | timestamps(updated_at: false)
16 | end
17 |
18 | create index(:users_actions, [:user_id])
19 | create index(:users_actions, [:context])
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20171005001838_create_users_actions_reports.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.CreateUsersActionsReports do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:users_actions_reports) do
6 | add :analyser_id, :integer, null: false
7 | add :last_action_id, :integer, null: false
8 | add :status, :integer, null: false
9 |
10 | # Various stats
11 | add :nb_actions, :integer, null: true
12 | add :nb_entries_updated, :integer, null: true
13 | add :run_duration, :integer, null: true
14 |
15 | timestamps()
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20171005215001_rename_flag_type_to_flag_entity_and_remove_unused_indexes.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.RenameFlagTypeToFlagEntityAndRemoveUnusedIndexes do
2 | use Ecto.Migration
3 |
4 | def change do
5 | rename table(:flags), :type, to: :entity
6 |
7 | # Rename unique index
8 | drop unique_index(:flags, [:source_user_id, :type, :entity_id])
9 | create unique_index(:flags, [:source_user_id, :entity, :entity_id])
10 |
11 | # Remove unused indexes
12 | drop index(:flags, [:source_user_id])
13 | drop index(:flags, [:target_user_id])
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20171009065840_delete_video_debate_action.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.DeleteVideoDebateAction do
2 | use Ecto.Migration
3 |
4 | def change do
5 | drop table(:video_debate_actions)
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20171026222425_add_today_reputation_gain_to_user.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.AddTodayReputationGainToUser do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:users) do
6 | add :today_reputation_gain, :integer, default: 0, null: false
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20171105124655_change_flags_to_flag_actions.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.ChangeFlagsToFlagActions do
2 | use Ecto.Migration
3 |
4 | alias DB.Repo
5 |
6 |
7 | @doc"""
8 | Flags used to point on entities, they will now point directly on actions
9 |
10 | [!] This will remove all old flags
11 | """
12 | def change do
13 | # Remove all entries and deprecated indexes
14 | Repo.delete_all(DB.Schema.Flag, log: false)
15 | drop unique_index(:flags, [:source_user_id, :entity, :entity_id])
16 |
17 | # Alter table
18 | alter table(:flags) do
19 | remove :entity
20 | remove :entity_id
21 | remove :target_user_id
22 | add :action_id, references(:users_actions, on_delete: :delete_all), null: false
23 | end
24 |
25 | # Create new unique index for user / action (1 flag per action max)
26 | create unique_index(:flags, [:source_user_id, :action_id], name: :user_flags_unique_index)
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20171109105152_create_moderation_users_feedbacks.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.CreateModerationUsersFeedbacks do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:moderation_users_feedbacks) do
6 | add :value, :integer
7 | add :user_id, references(:users, on_delete: :delete_all)
8 | add :action_id, references(:users_actions, on_delete: :delete_all)
9 |
10 | timestamps()
11 | end
12 |
13 | create unique_index(:moderation_users_feedbacks, [:user_id, :action_id])
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20171110040302_allow_null_user_on_user_action.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.AllowNullUserOnUserAction do
2 | @moduledoc"""
3 | This migration allow for null value in user_id which will represent an admin action
4 | """
5 |
6 | use Ecto.Migration
7 |
8 | def change do
9 | alter table(:users_actions) do
10 | # No need to use a reference user as it is already referenced by previous migration. Referencing again
11 | # fails as constraint already exists
12 | modify :user_id, :integer, null: true
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20171110212108_rename_comment_is_banned_to_is_reported.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.RenameCommentIsBannedToIsReported do
2 | use Ecto.Migration
3 |
4 | def change do
5 | drop index(:comments, [:statement_id], where: "is_banned = false")
6 | rename table(:comments), :is_banned, to: :is_reported
7 | create index(:comments, [:statement_id])
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20171117131508_add_slug_to_speakers.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.AddSlugToSpeaker do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:speakers) do
6 | add :slug, :string, null: true
7 | end
8 |
9 | create unique_index(:speakers, [:slug], where: "slug != NULL")
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20171119075520_delete_achievements.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.DeleteAchievements do
2 | use Ecto.Migration
3 |
4 | def change do
5 | drop table(:achievements)
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20171205174328_add_newsletter_to_user.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.AddNewsletterToUser do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:users) do
6 | add :newsletter, :boolean, null: false, default: true
7 | add :newsletter_subscription_token, :string, null: false, default: fragment("md5(random()::text)")
8 | end
9 |
10 | create unique_index(:users, :newsletter_subscription_token)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20180131002547_add_is_publisher_to_users.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.AddIsPublisherToUsers do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:users) do
6 | add :is_publisher, :boolean, default: false, null: false
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20180302024059_nilify_user_on_user_action_when_deleting.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.NilifyUserOnUserActionWhenDeleting do
2 | use Ecto.Migration
3 |
4 | def change do
5 | drop constraint("users_actions", "users_actions_user_id_fkey")
6 | drop constraint("users_actions", "users_actions_target_user_id_fkey")
7 |
8 | alter table(:users_actions) do
9 | modify :user_id, references(:users, on_delete: :nilify_all)
10 | modify :target_user_id, references(:users, on_delete: :nilify_all)
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20180317062636_add_unlisted_to_videos.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.AddUnlistedToVideos do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:videos) do
6 | add :unlisted, :boolean, default: false, null: false
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20180330204602_add_og_url_to_source.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.AddOgUrlToSource do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:sources) do
6 | add :og_url, :string, default: nil, null: true
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20180409035326_add_flag_reason_to_moderation_users_feedbacks.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.AddFlagReasonToModerationUsersFeedback do
2 | use Ecto.Migration
3 | import Ecto.Query
4 | alias DB.Schema.UserAction
5 |
6 | def change do
7 | # Delete all existing feedbacks
8 | DB.Repo.delete_all(DB.Schema.ModerationUserFeedback, log: false)
9 |
10 | # Add flag reason to feedbacks
11 | alter table("moderation_users_feedbacks") do
12 | add(:flag_reason, :integer, null: false)
13 | end
14 |
15 | # We also changed the way confirmed_email actions are recorded, invert
16 | # source_user_id and target_user_id
17 | UserAction
18 | |> where([a], a.type == ^:email_confirmed)
19 | |> where([a], is_nil(a.target_user_id))
20 | |> select([:id, :type, :user_id, :target_user_id])
21 | |> DB.Repo.all()
22 | |> Enum.map(&invert_source_and_target_users/1)
23 | end
24 |
25 | defp invert_source_and_target_users(action = %{user_id: src_usr_id}) do
26 | action
27 | |> Ecto.Changeset.change(user_id: nil, target_user_id: src_usr_id)
28 | |> DB.Repo.update(log: false)
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20180503083056_add_locale_to_invitation_request.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.AddLocaleToInvitationRequest do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:invitation_requests) do
6 | add :locale, :string, null: false, default: "en"
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20180516170544_add_is_partner_to_video.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.AddIsPartnerToVideo do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:videos) do
6 | add :is_partner, :boolean, default: false, null: false
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20180605085958_add_completed_onboarding_to_users.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.AddCompletedOnboardingToUsers do
2 | use Ecto.Migration
3 |
4 | def up do
5 | alter table "users" do
6 | add :completed_onboarding_steps, {:array, :integer}, null: false, default: []
7 | end
8 | end
9 |
10 | def down do
11 | alter table "users" do
12 | remove :completed_onboarding_steps
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20180605144832_add_speaker_to_user.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.AddSpeakerToUser do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:users) do
6 | add :speaker_id, references(:speakers, on_delete: :nilify_all), null: true, default: nil
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20180730092029_allow_null_user_for_comment.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.AllowNullUserForComment do
2 | use Ecto.Migration
3 |
4 | def change do
5 | drop constraint("comments", "comments_user_id_fkey")
6 |
7 | alter table(:comments) do
8 | modify :user_id, references(:users, on_delete: :nilify_all), null: true
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20180801105246_guardiandb.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.Repo.Migrations.Guardian.DB do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:guardian_tokens, primary_key: false) do
6 | add(:jti, :string, primary_key: true)
7 | add(:aud, :string, primary_key: true)
8 | add(:typ, :string)
9 | add(:iss, :string)
10 | add(:sub, :string)
11 | add(:exp, :bigint)
12 | add(:jwt, :text)
13 | add(:claims, :map)
14 | timestamps()
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20180802155107_make_speaker_slug_case_insensitive.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.MakeSpeakerSlugCaseInsensitive do
2 | use Ecto.Migration
3 |
4 | def up do
5 | alter table("speakers") do
6 | modify :slug, :citext
7 | end
8 | end
9 |
10 | def down do
11 | alter table("speakers") do
12 | modify :slug, :string
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20180803143819_increase_max_comment_text_length.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.IncreaseMaxCommentTextLength do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:comments) do
6 | modify :text, :string, size: 512
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20180816112748_change_wikidata_item_id_type_to_string.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.ChangeWikidataItemIdTypeToString do
2 | use Ecto.Migration
3 |
4 | def up do
5 | execute("""
6 | ALTER TABLE speakers
7 | ALTER COLUMN wikidata_item_id TYPE character varying(255)
8 | USING 'Q' || wikidata_item_id;
9 | """)
10 | end
11 |
12 | def down do
13 | execute("""
14 | ALTER TABLE speakers
15 | ALTER COLUMN wikidata_item_id TYPE integer
16 | USING (substring(wikidata_item_id from 2))::integer;
17 | """)
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20180816115534_remove_speaker_is_user_defined_column.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.RemoveSpeakerIsUserDefinedColumn do
2 | use Ecto.Migration
3 |
4 | def up do
5 | # Drop indexes
6 | drop(index(:speakers, :full_name))
7 | drop(index(:speakers, :wikidata_item_id))
8 | drop(unique_index(:speakers, :slug))
9 |
10 | # Remove column
11 | alter table(:speakers) do
12 | remove(:is_user_defined)
13 | remove(:is_removed)
14 | end
15 |
16 | # Re-create indexes
17 | create index(:speakers, :full_name)
18 | create unique_index(:speakers, :wikidata_item_id)
19 | create unique_index(:speakers, :slug, where: "slug IS NOT NULL")
20 | end
21 |
22 | def down do
23 | # Drop indexes
24 | drop(index(:speakers, :full_name))
25 | drop(index(:speakers, :wikidata_item_id))
26 | drop(unique_index(:speakers, :slug))
27 |
28 | # Re-add columns
29 | alter table(:speakers) do
30 | add(:is_user_defined, :boolean, null: false, default: true)
31 | add :is_removed, :boolean, null: false, default: false
32 | end
33 |
34 | # Re-create indexes
35 | create index(:speakers, :full_name, where: "is_user_defined = false")
36 | create unique_index(:speakers, :wikidata_item_id, where: "is_user_defined = false")
37 | create unique_index(:speakers, :slug, where: "slug IS NOT NULL")
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20180827123706_add_hash_id_to_videos.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.AddHashIdToVideos do
2 | use Ecto.Migration
3 | import Ecto.Query
4 | alias DB.Schema.Video
5 |
6 | def up do
7 | alter table(:videos) do
8 | # A size of 10 allows us to go up to 100_000_000_000_000 videos
9 | add(:hash_id, :string, size: 10)
10 | end
11 |
12 | # Create unique index on hash_id
13 | create(unique_index(:videos, [:hash_id]))
14 |
15 | # Flush pending migrations to ensure column is created
16 | flush()
17 |
18 | # Update all existing videos with their hashIds
19 | Video
20 | |> select([:id])
21 | |> DB.Repo.all()
22 | |> Enum.map(&Video.changeset_generate_hash_id/1)
23 | |> Enum.map(&DB.Repo.update/1)
24 | end
25 |
26 | def down do
27 | alter table(:videos) do
28 | remove(:hash_id)
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20181010105152_create_video_captions.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.CreateVideoCaptions do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:videos_captions) do
6 | add(:video_id, references(:videos, on_delete: :delete_all))
7 | add(:content, :text, null: false)
8 | add(:format, :string, null: false)
9 | timestamps()
10 | end
11 |
12 | create(index(:videos_captions, [:video_id]))
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20181109223648_increate_source_max_url_length.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.IncreateSourceMaxUrlLength do
2 | use Ecto.Migration
3 |
4 | def up do
5 | alter table(:sources) do
6 | modify(:url, :string, size: 2048)
7 | end
8 | end
9 |
10 | def down do
11 | Ecto.Adapters.SQL.query!(DB.Repo, """
12 | DELETE FROM sources WHERE LENGTH(url) > 255;
13 | """)
14 |
15 | alter table(:sources) do
16 | modify(:url, :string)
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20181109233422_add_file_type_to_source.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.AddFileTypeToSource do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:sources) do
6 | add(:file_mime_type, :string, null: true)
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20181209205427_videos_providers_as_columns.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.VideosProvidersAsColumns do
2 | @moduledoc """
3 | This migration changes the `Videos` schema to go from a pair
4 | of {provider, provider_id} to a model where we have multiple `{provider}_id`
5 | column. This will allow to store multiple sources for a single video, with
6 | different offsets to ensure they're all in sync.
7 | """
8 |
9 | use Ecto.Migration
10 |
11 | def up do
12 | # Add new columns
13 | alter table(:videos) do
14 | add(:youtube_id, :string, null: true, length: 11)
15 | add(:youtube_offset, :integer, null: false, default: 0)
16 | end
17 |
18 | flush()
19 |
20 | # Migrate existing videos - we only have YouTube right now
21 | Ecto.Adapters.SQL.query!(DB.Repo, """
22 | UPDATE videos SET youtube_id = provider_id;
23 | """)
24 |
25 | flush()
26 |
27 | # Create index
28 | create(unique_index(:videos, :youtube_id))
29 |
30 | # Remove columns
31 | alter table(:videos) do
32 | remove(:provider)
33 | remove(:provider_id)
34 | end
35 | end
36 |
37 | def down do
38 | # Restore old scheme
39 | alter table(:videos) do
40 | add(:provider, :string)
41 | add(:provider_id, :string)
42 | end
43 |
44 | flush()
45 |
46 | # Migrate existing videos
47 | Ecto.Adapters.SQL.query!(DB.Repo, """
48 | UPDATE videos SET provider_id = youtube_id, provider = 'youtube';
49 | """)
50 |
51 | flush()
52 |
53 | # Re-create index
54 | create(unique_index(:videos, [:provider, :provider_id]))
55 |
56 | # Remove columns
57 | alter table(:videos) do
58 | remove(:youtube_id)
59 | remove(:youtube_offset)
60 | end
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20190110165430_create_subscriptions.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.Createsubscriptions do
2 | use Ecto.Migration
3 |
4 | def up do
5 | DB.Type.SubscriptionReason.create_type()
6 |
7 | create table(:subscriptions) do
8 | add(:user_id, references(:users, on_delete: :delete_all), null: false)
9 |
10 | add(:video_id, references(:videos, on_delete: :delete_all), null: false)
11 | add(:statement_id, references(:statements, on_delete: :delete_all))
12 | add(:comment_id, references(:comments, on_delete: :delete_all))
13 | add(:scope, :integer, null: false)
14 |
15 | add(:reason, :subscription_reason)
16 | add(:is_subscribed, :boolean, default: true, null: false)
17 | end
18 |
19 | create(unique_index(:subscriptions, [:user_id, :video_id, :statement_id, :comment_id]))
20 | end
21 |
22 | def down do
23 | drop(table(:subscriptions))
24 | DB.Type.SubscriptionReason.drop_type()
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20190110171405_create_notifications.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.CreateNotifications do
2 | use Ecto.Migration
3 |
4 | def up do
5 | DB.Type.NotificationType.create_type()
6 |
7 | create table(:notifications) do
8 | add(:user_id, references(:users, on_delete: :delete_all), null: false)
9 | add(:action_id, references(:users_actions, on_delete: :delete_all), null: false)
10 | add(:type, :notification_type, null: false)
11 | add(:seen_at, :utc_datetime, null: true, default: nil)
12 |
13 | timestamps()
14 | end
15 |
16 | create(index(:notifications, [:user_id, :action_id]))
17 | end
18 |
19 | def down do
20 | drop(index(:notifications, [:user_id, :action_id]))
21 | drop(table(:notifications))
22 | DB.Type.NotificationType.drop_type()
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20200224124536_add-facebook-id-to-videos.exs:
--------------------------------------------------------------------------------
1 | defmodule :"Elixir.DB.Repo.Migrations.Add-facebook-id-to-videos" do
2 | use Ecto.Migration
3 |
4 | def up do
5 | # Add new columns
6 | alter table(:videos) do
7 | add(:facebook_id, :string, null: true)
8 | add(:facebook_offset, :integer, null: false, default: 0)
9 | end
10 |
11 | # Create index
12 | create(unique_index(:videos, :facebook_id))
13 | end
14 |
15 | def down do
16 | # Remove columns
17 | alter table(:videos) do
18 | remove(:facebook_id)
19 | remove(:facebook_offset)
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20200224211412_add-thumbnail-to-videos.exs:
--------------------------------------------------------------------------------
1 | defmodule :"Elixir.DB.Repo.Migrations.Add-thumbnail-to-videos" do
2 | use Ecto.Migration
3 |
4 | def up do
5 | # Add new columns
6 | alter table(:videos) do
7 | add(:thumbnail, :string, null: true)
8 | end
9 |
10 | flush()
11 |
12 | Ecto.Adapters.SQL.query!(DB.Repo, """
13 | UPDATE videos
14 | SET thumbnail = 'https://img.youtube.com/vi/' || youtube_id || '/hqdefault.jpg'
15 | WHERE youtube_id IS NOT NULL;
16 | """)
17 | end
18 |
19 | def down do
20 | # Remove columns
21 | alter table(:videos) do
22 | remove(:thumbnail)
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20210930122534_increase_statement_max_length.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.IncreaseStatementMaxLength do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:statements) do
6 | modify :text, :string, size: 280
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20240618055503_update_video_captions.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Repo.Migrations.UpdateVideoCaptions do
2 | use Ecto.Migration
3 |
4 | def up do
5 | # Delete all values (there are none in prod)
6 | execute("DELETE FROM videos_captions")
7 |
8 | # Drop column :content in favor of raw + parsed
9 | alter table(:videos_captions) do
10 | remove(:content)
11 | add(:raw, :text, null: false)
12 | add(:parsed, {:array, :map}, null: false)
13 | end
14 | end
15 |
16 | def down do
17 | # Drop raw + parsed in favor of :content
18 | alter table(:videos_captions) do
19 | remove(:raw)
20 | remove(:parsed)
21 | add(:content, :text, null: false)
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/migrations/20240915080224_add-draft-to-statements.exs:
--------------------------------------------------------------------------------
1 | defmodule :"Elixir.DB.Repo.Migrations.Add-draft-to-statements" do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:statements) do
6 | add :is_draft, :boolean, default: false, null: false
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/seed_with_csv.exs:
--------------------------------------------------------------------------------
1 | defmodule SeedWithCSV do
2 | require Logger
3 |
4 | @nb_threads 1
5 | @timeout 60_000
6 |
7 | def seed(filename, insert_func, insert_func_args, columns_mapping) do
8 | if File.exists?(filename),
9 | do: do_seed(filename, insert_func, insert_func_args, columns_mapping),
10 | else: Logger.error("File #{filename} doesn't exists")
11 | end
12 |
13 | defp do_seed(filename, insert_func, insert_func_args, columns_mapping) do
14 | filename
15 | |> File.stream!()
16 | |> CSV.decode(headers: true)
17 | |> Task.async_stream(
18 | &build_and_insert(&1, insert_func, insert_func_args, columns_mapping),
19 | max_concurrency: @nb_threads,
20 | timeout: @timeout
21 | )
22 | |> Enum.to_list()
23 | end
24 |
25 | defp build_and_insert(entry, insert_func, insert_func_args, columns_mapping) do
26 | changes =
27 | entry
28 | |> Enum.filter(fn {key, _} -> Map.has_key?(columns_mapping, key) end)
29 | |> Enum.map(fn {key, value} ->
30 | case Map.get(columns_mapping, key) do
31 | key when is_atom(key) or is_binary(key) ->
32 | {key, value}
33 |
34 | {key, func} when is_atom(key) or (is_binary(key) and is_function(func)) ->
35 | {key, func.(value)}
36 | end
37 | end)
38 | |> Enum.into(%{})
39 |
40 | insert_func.(changes, insert_func_args)
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/apps/db/priv/repo/seeds.exs:
--------------------------------------------------------------------------------
1 | alias DB.Repo
2 | alias DB.Schema.User
3 | require Logger
4 |
5 | # Create Admin in dev or if we're running image locally
6 | if Application.get_env(:db, :env) == :dev do
7 | Logger.warn("API is running in dev mode. Inserting default user admin@captainfact.io")
8 |
9 | admin =
10 | User.registration_changeset(%User{reputation: 4200, username: "Captain"}, %{
11 | email: "admin@captainfact.io",
12 | password: "password"
13 | })
14 |
15 | # No need to warn if already exists
16 | Repo.insert(admin)
17 | end
18 |
--------------------------------------------------------------------------------
/apps/db/priv/secrets/db_hostname:
--------------------------------------------------------------------------------
1 | localhost
--------------------------------------------------------------------------------
/apps/db/priv/secrets/db_name:
--------------------------------------------------------------------------------
1 | captain_fact_dev
--------------------------------------------------------------------------------
/apps/db/priv/secrets/db_password:
--------------------------------------------------------------------------------
1 | postgres
--------------------------------------------------------------------------------
/apps/db/priv/secrets/db_username:
--------------------------------------------------------------------------------
1 | postgres
--------------------------------------------------------------------------------
/apps/db/test/db_schema/flag_test.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Schema.FlagTest do
2 | use DB.DataCase, async: true
3 |
4 | alias DB.Schema.Flag
5 |
6 | test "changeset with valid attributes" do
7 | changeset = Flag.changeset(%Flag{source_user_id: 42}, %{reason: 1, action_id: 42})
8 | assert changeset.valid?
9 | end
10 |
11 | test "reason cannot be anything" do
12 | changeset = Flag.changeset(%Flag{source_user_id: 42}, %{reason: 0, action_id: 42})
13 | refute changeset.valid?
14 |
15 | changeset = Flag.changeset(%Flag{source_user_id: 42}, %{reason: 4500, action_id: 42})
16 | refute changeset.valid?
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/apps/db/test/db_schema/moderation_user_feedback_test.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Schema.ModerationUserFeedbackTest do
2 | use DB.DataCase, async: true
3 | alias DB.Schema.ModerationUserFeedback
4 | import DB.Schema.ModerationUserFeedback, only: [changeset: 2]
5 |
6 | @valid_attrs %{value: 1, flag_reason: 1}
7 | @base_feedback %ModerationUserFeedback{user_id: 1, action_id: 1}
8 |
9 | test "changeset with valid attributes" do
10 | assert changeset(@base_feedback, @valid_attrs).valid?
11 | end
12 |
13 | test "feedback value can only be +1, 0 or -1" do
14 | assert {:value, "must be greater than or equal to -1"} in errors_on(@base_feedback, %{
15 | value: -2
16 | })
17 |
18 | assert {:value, "must be less than or equal to 1"} in errors_on(@base_feedback, %{value: 10})
19 | assert {:value, "is invalid"} in errors_on(@base_feedback, %{value: "Hello"})
20 | end
21 |
22 | test "reason cannot be anything" do
23 | refute changeset(@base_feedback, %{@valid_attrs | flag_reason: 5000}).valid?
24 | refute changeset(@base_feedback, %{@valid_attrs | flag_reason: 0}).valid?
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/apps/db/test/db_schema/notification_test.exs:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/apps/db/test/db_schema/statement_test.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Schema.StatementTest do
2 | use DB.DataCase, async: true
3 |
4 | alias DB.Schema.Statement
5 |
6 | @valid_attrs %{
7 | text: "Be proud of you Because you can be do what we want to do !",
8 | time: 42,
9 | speaker_id: 3,
10 | video_id: 2
11 | }
12 | @invalid_attrs %{}
13 |
14 | test "changeset with valid attributes" do
15 | changeset = Statement.changeset(%Statement{}, @valid_attrs)
16 | assert changeset.valid?
17 | end
18 |
19 | test "changeset with invalid attributes" do
20 | changeset = Statement.changeset(%Statement{}, @invalid_attrs)
21 | refute changeset.valid?
22 | end
23 |
24 | test "time cannot be negative" do
25 | attrs = Map.put(@valid_attrs, :time, -1)
26 | changeset = Statement.changeset(%Statement{}, attrs)
27 | refute changeset.valid?
28 | end
29 |
30 | test "text cannot be empty" do
31 | assert {:text, "can't be blank"} in errors_on(%Statement{}, %{text: ""})
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/apps/db/test/db_schema/user_action_test.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Schema.UserActionTest do
2 | use DB.DataCase, async: true
3 |
4 | alias DB.Schema.UserAction
5 |
6 | @valid_attrs %{
7 | user_id: 1,
8 | entity: :statement,
9 | comment_id: 42,
10 | type: :update,
11 | changes: %{
12 | text: "Updated !"
13 | }
14 | }
15 | @invalid_attrs %{}
16 |
17 | test "changeset with valid attributes" do
18 | changeset = UserAction.changeset(%UserAction{}, @valid_attrs)
19 | assert changeset.valid?
20 | end
21 |
22 | test "changeset with invalid attributes" do
23 | changeset = UserAction.changeset(%UserAction{}, @invalid_attrs)
24 | refute changeset.valid?
25 | end
26 |
27 | test "entity cannot be anything" do
28 | attrs = Map.put(@valid_attrs, :entity, "Not a valid entity !")
29 | changeset = UserAction.changeset(%UserAction{}, attrs)
30 | refute changeset.valid?
31 | end
32 |
33 | test "action type must be create, remove, update, delete, or add" do
34 | attrs = Map.put(@valid_attrs, :type, "invalid_action_type")
35 | changeset = UserAction.changeset(%UserAction{}, attrs)
36 | refute changeset.valid?
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/apps/db/test/db_schema/vote_test.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Schema.VoteTest do
2 | use DB.DataCase, async: true
3 | doctest DB.Schema.Vote
4 |
5 | alias DB.Schema.Vote
6 |
7 | @valid_attrs %{user_id: 1, comment_id: 1, value: 1}
8 | @invalid_attrs %{}
9 |
10 | test "changeset with valid attributes" do
11 | changeset = Vote.changeset(%Vote{}, @valid_attrs)
12 | assert changeset.valid?
13 |
14 | changeset = Vote.changeset(%Vote{}, Map.put(@valid_attrs, :value, -1))
15 | assert changeset.valid?
16 | end
17 |
18 | test "changeset with invalid attributes" do
19 | changeset = Vote.changeset(%Vote{}, @invalid_attrs)
20 | refute changeset.valid?
21 | end
22 |
23 | test "vote value can only be +1 or -1" do
24 | assert {:value, "is invalid"} in errors_on(%Vote{}, %{value: -2})
25 | assert {:value, "is invalid"} in errors_on(%Vote{}, %{value: 10})
26 | assert {:value, "is invalid"} in errors_on(%Vote{}, %{value: "Hello"})
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/apps/db/test/db_type/achievement_test.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Type.AchievementTest do
2 | use ExUnit.Case
3 |
4 | alias DB.Type.Achievement
5 |
6 | test "ensure values don't change" do
7 | assert Achievement.get(:welcome) == 1
8 | assert Achievement.get(:not_a_robot) == 2
9 | assert Achievement.get(:help) == 3
10 | assert Achievement.get(:bulletproof) == 4
11 | assert Achievement.get(:you_are_fake_news) == 5
12 | assert Achievement.get(:social_networks) == 6
13 | assert Achievement.get(:ambassador) == 7
14 | # Made a bug report
15 | assert Achievement.get(:ghostbuster) == 8
16 | # Leaderboard
17 | assert Achievement.get(:famous) == 9
18 | end
19 |
20 | test "doesn't compile if bad value" do
21 | assert_raise FunctionClauseError, fn ->
22 | Achievement.get(:nopenopenope)
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/apps/db/test/db_type/user_picture.ex:
--------------------------------------------------------------------------------
1 | defmodule DB.Type.UserPictureTest do
2 | use DB.DataCase, async: true
3 | doctest DB.Schema.User
4 |
5 | import DB.Factory, only: [insert: 1, insert: 2]
6 | alias DB.Schema.User
7 |
8 | test "defaults to gravatar" do
9 | user = insert(:user, picture_url: nil)
10 | email_md5 = :crypto.hash(:md5, user.email) |> Base.encode16(case: :lower)
11 |
12 | assert DB.Type.UserPicture.default_url(:thumb, user) ==
13 | "https://gravatar.com/avatar/#{email_md5}.jpg?size=94&d=robohash"
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/apps/db/test/db_type/video_hash_id_test.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Type.VideoHashIdTest do
2 | use ExUnit.Case
3 | use ExUnitProperties
4 |
5 | alias DB.Type.VideoHashId
6 |
7 | doctest VideoHashId
8 |
9 | @nb_ids_to_test 1_000
10 |
11 | @tag timeout: 3_600_000
12 | test "ensure there is no collision" do
13 | start = 1
14 | range = start..(start + @nb_ids_to_test)
15 |
16 | uniq_generated_ids =
17 | range
18 | |> Enum.map(&VideoHashId.encode/1)
19 | |> Enum.into(MapSet.new())
20 |
21 | assert Enum.count(uniq_generated_ids) == Enum.count(range)
22 | end
23 |
24 | property "should work with any integer" do
25 | check(all(id <- id_generator(), do: assert(String.length(VideoHashId.encode(id)) >= 4)))
26 | end
27 |
28 | defp id_generator do
29 | ExUnitProperties.gen all(id <- integer()) do
30 | abs(id)
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/apps/db/test/db_utils/string_test.exs:
--------------------------------------------------------------------------------
1 | defmodule DB.Utils.StringTest do
2 | use ExUnit.Case
3 | doctest DB.Utils.String
4 | end
5 |
--------------------------------------------------------------------------------
/apps/db/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | # Start everything
2 |
3 | Faker.start()
4 |
5 | Ecto.Adapters.SQL.Sandbox.mode(DB.Repo, {:shared, self()})
6 | {:ok, _} = Application.ensure_all_started(:ex_machina)
7 |
8 | ExUnit.start()
9 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | import_config "../apps/*/config/config.exs"
4 | import_config "./*.secret.exs" # TODO should filter by env
5 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule CF.Umbrella.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | version: "1.2.0",
7 | apps_path: "apps",
8 | deps_path: "deps",
9 | build_embedded: Mix.env() == :prod,
10 | start_permanent: Mix.env() == :prod,
11 | deps: deps(),
12 | aliases: aliases(),
13 | test_coverage: [tool: ExCoveralls],
14 | preferred_cli_env: [
15 | coveralls: :test,
16 | "coveralls.detail": :test,
17 | "coveralls.post": :test,
18 | "coveralls.html": :test
19 | ],
20 | releases: [
21 | full_app: [
22 | applications: [
23 | cf_reverse_proxy: :permanent,
24 | cf_jobs: :permanent
25 | ]
26 | ]
27 | ]
28 | ]
29 | end
30 |
31 | defp deps do
32 | [
33 | # ---- Test and Dev
34 | {:excoveralls, "~> 0.12.1", only: :test},
35 | {:credo, "~> 1.7.5", only: [:dev, :test], runtime: false},
36 | {:mix_test_watch, "~> 1.1", only: :dev, runtime: false}
37 | ]
38 | end
39 |
40 | defp aliases do
41 | [
42 | "ecto.setup": ["ecto.create", "ecto.migrate", "ecto.seed"],
43 | "ecto.seed": ["run apps/db/priv/repo/seeds.exs"],
44 | "ecto.reset": ["ecto.drop", "ecto.setup"],
45 | test: ["ecto.create --quiet", "ecto.migrate", "test"]
46 | ]
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/scripts/download-captions.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # A simple script that uses yt-dlp to download captions
3 | # Usage: ./download-captions.sh [--locale=locale]
4 |
5 |
6 |
7 |
8 | # Formats: vtt/ttml/srv3/srv2/srv1/json3
9 |
10 | yt-dlp --write-auto-sub --skip-download --sub-langs fr --sub-format srv1 --output "subtitles.%(ext)s" "$1"
--------------------------------------------------------------------------------
/scripts/run_e2e_ci.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | cd "$(dirname "$(realpath "$0")")"/..
4 |
5 | # Start API
6 | cd ./api
7 | mix run --no-halt &
8 |
9 | # Start Frontend
10 | cd ../frontend
11 | npm run dev &
12 |
13 | # Waiting for API to be ready
14 | timeout 1m bash -c "until curl localhost:4000; do sleep 1; done"
15 |
16 | # Waiting for Frontend to be ready
17 | timeout 1m bash -c "until curl localhost:3333 > /dev/null; do sleep 1; done"
18 |
19 | # Run tests
20 | npm run cypress
21 |
--------------------------------------------------------------------------------
/scripts/test_release.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # Build the release and test it against a dev environment. Yay!
3 | # ------------------------------------------------------------------
4 |
5 | export MIX_ENV=prod
6 | export CF_SECRET_KEY_BASE="8C6FsJwjV11d+1WPUIbkEH6gB/VavJrcXWoPLujgpclfxjkLkoNFSjVU9XfeNm6s"
7 | export CF_HOST=localhost
8 | export CF_DB_HOSTNAME=localhost
9 | export CF_DB_USERNAME=postgres
10 | export CF_DB_PASSWORD=postgres
11 | export CF_DB_NAME=captain_fact_dev
12 | export CF_FACEBOOK_APP_ID=xxxxxxxxxxxxxxxxxxxx
13 | export CF_FACEBOOK_APP_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
14 | export CF_FRONTEND_URL="http://localhost:3333"
15 | export CF_CHROME_EXTENSION_ID="chrome-extension://fnnhlmbnlbgomamcolcpgncflofhjckm"
16 | export CF_PORT=4242
17 |
18 | # With Mix
19 | # mix release --overwrite
20 | # _build/prod/rel/full_app/bin/full_app start
21 |
22 | # With Docker
23 | docker build -t cf-test-release .
24 | docker run \
25 | -e MIX_ENV \
26 | -e CF_SECRET_KEY_BASE \
27 | -e CF_HOST \
28 | -e CF_DB_HOSTNAME \
29 | -e CF_DB_USERNAME \
30 | -e CF_DB_PASSWORD \
31 | -e CF_DB_NAME \
32 | -e CF_FACEBOOK_APP_ID \
33 | -e CF_FACEBOOK_APP_SECRET \
34 | -e CF_FRONTEND_URL \
35 | -e CF_CHROME_EXTENSION_ID \
36 | -e CF_PORT \
37 | --network="host" \
38 | cf-test-release
39 |
--------------------------------------------------------------------------------