-
Notifications
You must be signed in to change notification settings - Fork 56
Integration of Chatkit #406
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6dfbb12
6185ec0
4c5026b
5e447cf
018690c
4691dac
334231f
5cb5ddf
80fcc0b
f27ae9e
9a3243b
472ebca
2cfeae2
8491f78
1c3e241
9e66f58
fdea41c
fefc2a5
6bf7608
3e851e4
325e417
51a0c9b
3604b39
574472e
b442afa
3d3915b
62d2f11
060ec2f
e0e84bf
668beb5
42aa925
2b01b52
4a628d9
039e5fa
e69e7f0
a9b44d5
d864c68
a996615
b3ba7a4
78c41a9
19474a9
ff5fbcf
e709d0c
3dc73c2
d5c1fdf
aa75e3f
21de4ea
05b830d
842e534
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ defmodule Cadet.Assessments do | |
| alias Cadet.Accounts.User | ||
| alias Cadet.Assessments.{Answer, Assessment, Query, Question, Submission} | ||
| alias Cadet.Autograder.GradingJob | ||
| alias Cadet.Chat.Room | ||
| alias Ecto.Multi | ||
|
|
||
| @xp_early_submission_max_bonus 100 | ||
|
|
@@ -363,8 +364,13 @@ defmodule Cadet.Assessments do | |
| {:is_open?, true} <- is_open?(question.assessment), | ||
| {:ok, submission} <- find_or_create_submission(user, question.assessment), | ||
| {:status, true} <- {:status, submission.status != :submitted}, | ||
| {:ok, _} <- insert_or_update_answer(submission, question, raw_answer) do | ||
| {:ok, answer} <- insert_or_update_answer(submission, question, raw_answer) do | ||
| update_submission_status(submission, question.assessment) | ||
|
|
||
| if answer.comment == nil do | ||
| Room.create_rooms(submission, answer, user) | ||
| end | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, can this be placed where a submission occurs?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. May I clarify what you mean? Do you mean when a submission is being finalised?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. I think it is fine to leave the function here for now. But ultimately room creation should be done when a submission is submitted right?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure. I'll place it here then? Since this is where all submissions flow through, whether it's submitted early or submitted by the autograder. Quite ugly tho :/ |
||
|
|
||
| {:ok, nil} | ||
| else | ||
| {:question_found?, false} -> {:error, {:not_found, "Question not found"}} | ||
|
|
@@ -468,7 +474,6 @@ defmodule Cadet.Assessments do | |
| xp_adjustment: 0, | ||
| autograding_status: :none, | ||
| autograding_results: [], | ||
| comment: nil, | ||
| grader_id: nil | ||
| }) | ||
| |> Repo.update()} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| defmodule Cadet.Chat.Room do | ||
| @moduledoc """ | ||
| Contains logic pertaining to chatroom creation to supplement ChatKit, an external service engaged for Source Academy. | ||
| ChatKit's API can be found here: https://pusher.com/docs/chatkit | ||
| """ | ||
|
|
||
| require Logger | ||
|
|
||
| import Ecto.Query | ||
|
|
||
| alias Cadet.Repo | ||
| alias Cadet.Assessments.{Answer, Submission} | ||
| alias Cadet.Accounts.User | ||
| alias Cadet.Chat.Token | ||
|
|
||
| @instance_id (if Mix.env() != :test do | ||
| :cadet |> Application.fetch_env!(:chat) |> Keyword.get(:instance_id) | ||
| else | ||
| "instance_id" | ||
| end) | ||
|
|
||
| @doc """ | ||
| Creates a chatroom for every answer, and updates db with the chatroom id. | ||
| """ | ||
| def create_rooms( | ||
| %Submission{ | ||
| assessment_id: assessment_id | ||
| }, | ||
| answer = %Answer{question_id: question_id, comment: comment}, | ||
| user | ||
| ) do | ||
| with true <- comment == nil, | ||
| {:ok, %{"id" => room_id}} <- create_room(assessment_id, question_id, user) do | ||
| answer | ||
| |> Answer.comment_changeset(%{ | ||
| comment: room_id | ||
| }) | ||
| |> Repo.update() | ||
| end | ||
| end | ||
|
|
||
| defp create_room( | ||
| assessment_id, | ||
| question_id, | ||
| %User{ | ||
| id: student_id, | ||
| nusnet_id: nusnet_id | ||
| } | ||
| ) do | ||
| HTTPoison.start() | ||
|
|
||
| url = "https://us1.pusherplatform.io/services/chatkit/v4/#{@instance_id}/rooms" | ||
|
|
||
| {:ok, token} = Token.get_superuser_token() | ||
| headers = [Authorization: "Bearer #{token}"] | ||
|
|
||
| body = | ||
| Poison.encode!(%{ | ||
| "name" => "#{nusnet_id}_#{assessment_id}_Q#{question_id}", | ||
| "private" => true, | ||
| "user_ids" => get_staff_admin_user_ids() ++ [to_string(student_id)] | ||
| }) | ||
|
|
||
| case HTTPoison.post(url, body, headers) do | ||
| {:ok, %HTTPoison.Response{body: body, status_code: 201}} -> | ||
| Poison.decode(body) | ||
|
|
||
| {:ok, %HTTPoison.Response{body: body, status_code: status_code}} -> | ||
| response_body = Poison.decode!(body) | ||
|
|
||
| Logger.error( | ||
| "Room creation failed: #{response_body["error"]}, " <> | ||
| "#{response_body["error_description"]} (status code #{status_code}) " <> | ||
| "[user_id: #{student_id}, assessment_id: #{assessment_id}, question_id: #{question_id}]" | ||
| ) | ||
|
|
||
| {:error, nil} | ||
|
|
||
| {:error, %HTTPoison.Error{reason: error}} -> | ||
| Logger.error("error: #{inspect(error, pretty: true)}") | ||
| {:error, nil} | ||
| end | ||
| end | ||
|
|
||
| defp get_staff_admin_user_ids do | ||
| User | ||
| |> where([u], u.role in ^[:staff, :admin]) | ||
| |> Repo.all() | ||
| |> Enum.map(fn user -> to_string(user.id) end) | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| defmodule Cadet.Chat.Token do | ||
| @moduledoc """ | ||
| Contains logic pertaining to the generation of tokens to supplement the usage of ChatKit, an external service engaged for Source Academy. | ||
| ChatKit's API can be found here: https://pusher.com/docs/chatkit | ||
| """ | ||
|
|
||
| alias Cadet.Accounts.User | ||
|
|
||
| @instance_id :cadet |> Application.fetch_env!(:chat) |> Keyword.get(:instance_id) | ||
| @key_id :cadet |> Application.fetch_env!(:chat) |> Keyword.get(:key_id) | ||
| @key_secret :cadet |> Application.fetch_env!(:chat) |> Keyword.get(:key_secret) | ||
| @token_ttl 86_400 | ||
| @admin_user_id "admin" | ||
|
|
||
| @doc """ | ||
| Generates user token for connection to ChatKit's ChatManager. | ||
| Returns {:ok, token, ttl}. | ||
| """ | ||
| def get_user_token(%User{id: user_id}) do | ||
| {:ok, token} = get_token(to_string(user_id)) | ||
| {:ok, token, @token_ttl} | ||
| end | ||
|
|
||
| @doc """ | ||
| Generates a token for user with admin rights to enable superuser permissions. | ||
| Returns {:ok, token} | ||
| """ | ||
| def get_superuser_token do | ||
| get_token(@admin_user_id, true) | ||
| end | ||
|
|
||
| defp get_token(user_id, su \\ false) when is_binary(user_id) do | ||
| curr_time_epoch = DateTime.to_unix(DateTime.utc_now()) | ||
|
|
||
| payload = %{ | ||
| "instance" => @instance_id, | ||
| "iss" => "api_keys/#{@key_id}", | ||
| "exp" => curr_time_epoch + @token_ttl, | ||
| "iat" => curr_time_epoch, | ||
| "sub" => user_id, | ||
| "su" => su | ||
| } | ||
|
|
||
| # Note: dialyzer says signing only returns {:ok, token} | ||
| Joken.Signer.sign( | ||
| payload, | ||
| Joken.Signer.create("HS256", @key_secret) | ||
| ) | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| defmodule CadetWeb.ChatController do | ||
| @moduledoc """ | ||
| Provides token for connection to ChatKit's server. | ||
| Refer to ChatKit's API here: https://pusher.com/docs/chatkit | ||
| """ | ||
|
|
||
| use CadetWeb, :controller | ||
| use PhoenixSwagger | ||
|
|
||
| alias Cadet.Chat.Token | ||
|
|
||
| def index(conn, _) do | ||
| user = conn.assigns.current_user | ||
| {:ok, token, ttl} = Token.get_user_token(user) | ||
|
|
||
| render( | ||
| conn, | ||
| "index.json", | ||
| access_token: token, | ||
| expires_in: ttl | ||
| ) | ||
| end | ||
|
|
||
| swagger_path :index do | ||
| post("/chat/token") | ||
|
|
||
| summary("Get the ChatKit bearer token of a user. Token expires in 24 hours.") | ||
|
|
||
| security([%{JWT: []}]) | ||
|
|
||
| produces("application/json") | ||
|
|
||
| response(200, "OK", Schema.ref(:TokenInfo)) | ||
| end | ||
|
|
||
| def swagger_definitions do | ||
| %{ | ||
| TokenInfo: | ||
| swagger_schema do | ||
| title("ChatKit Token") | ||
| description("Token used for connection to ChatKit's server") | ||
|
|
||
| properties do | ||
| access_token(:string, "Bearer token", required: true) | ||
|
|
||
| expires_in(:string, "TTL of token", required: true) | ||
| end | ||
| end | ||
| } | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| defmodule CadetWeb.ChatView do | ||
| use CadetWeb, :view | ||
|
|
||
| def render("index.json", %{access_token: token, expires_in: ttl}) do | ||
| %{ | ||
| access_token: token, | ||
| expires_in: ttl | ||
| } | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| defmodule Mix.Tasks.Cadet.ChatkitRoom do | ||
| @moduledoc """ | ||
| Creates ChatKit rooms for answers with empty comments in the database. | ||
| Room creation: https://pusher.com/docs/chatkit/reference/api#create-a-room | ||
| Status codes: https://pusher.com/docs/chatkit/reference/api#response-and-error-codes | ||
|
|
||
| Note: | ||
| - Task is to run daily | ||
| """ | ||
| use Mix.Task | ||
|
|
||
| import Ecto.Query | ||
| import Mix.EctoSQL | ||
|
|
||
| alias Cadet.Repo | ||
| alias Cadet.Assessments.Submission | ||
| alias Cadet.Chat.Room | ||
|
|
||
| def run(_args) do | ||
| ensure_started(Repo, []) | ||
|
|
||
| Submission | ||
| |> join(:inner, [s], a in assoc(s, :answers)) | ||
| |> join(:inner, [s], u in assoc(s, :student)) | ||
| |> preload([_, a, u], answers: a, student: u) | ||
| |> where([_, a], is_nil(a.comment)) | ||
| |> Repo.all() | ||
| |> Enum.each(fn submission -> | ||
| Enum.each(submission.answers, fn answer -> | ||
| Room.create_rooms(submission, answer, submission.student) | ||
| end) | ||
| end) | ||
| end | ||
| end |
Uh oh!
There was an error while loading. Please reload this page.