Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
6dfbb12
Logic for jwt generation used for chatkit complete
flxffy Jun 16, 2019
6185ec0
Merge branch 'master' of https://github.com/source-academy/cadet
flxffy Jun 16, 2019
4c5026b
/chat/token controller
flxffy Jun 17, 2019
5e447cf
Set up API for chat token generation
flxffy Jun 17, 2019
018690c
Documentation for ChatKit token generation
flxffy Jun 17, 2019
4691dac
Changed chatkit user identifier to user.id from db
flxffy Jun 23, 2019
334231f
Merge branch 'master' of https://github.com/source-academy/cadet
flxffy Jun 23, 2019
5cb5ddf
update readme with chatkit instructions
flxffy Jun 23, 2019
80fcc0b
code readbility
flxffy Jun 23, 2019
f27ae9e
Added private functions in chat token generator
flxffy Jun 23, 2019
9a3243b
fixed typo in superuser name
flxffy Jun 23, 2019
472ebca
Main logic for script to create all users
flxffy Jun 23, 2019
2cfeae2
Script for creating all chatkit users
flxffy Jun 23, 2019
8491f78
Add comment related functions
wardetu Jun 27, 2019
1c3e241
Storage of ChatKit room id. Logic complete with failsafes in thevent …
flxffy Jun 30, 2019
9e66f58
Remove test file
flxffy Jun 30, 2019
fdea41c
Refactored chat.ex
flxffy Jul 2, 2019
fefc2a5
Updated moduledoc for cadet.users.chat
flxffy Jul 2, 2019
6bf7608
Fixed unsubmit test
flxffy Jul 2, 2019
3e851e4
Create chatkit room when submission is retrieved only if authorised
flxffy Jul 2, 2019
325e417
fixed answer controller test case clause error
flxffy Jul 2, 2019
51a0c9b
Merge branch 'master' of https://github.com/source-academy/cadet
flxffy Jul 2, 2019
3604b39
Merge branch 'id_storage'
flxffy Jul 2, 2019
574472e
grading_controller_test.ex to accept unchanged comment field for unsu…
flxffy Jul 2, 2019
b442afa
Update README.md
flxffy Jul 3, 2019
3d3915b
Changed import to alias
flxffy Jul 3, 2019
62d2f11
Update assessments.ex
wardetu Jul 4, 2019
060ec2f
Update README.md
wardetu Jul 4, 2019
e0e84bf
Update README.md
wardetu Jul 4, 2019
668beb5
mix task update documentation
flxffy Jul 4, 2019
42aa925
Script for creating rooms as failsafe
flxffy Jul 4, 2019
2b01b52
Scheduled script to run daily at 1am
flxffy Jul 4, 2019
4a628d9
Changed import to alias
flxffy Jul 5, 2019
039e5fa
Changed grading_changeset and changeset to exclude comment when updat…
flxffy Jul 5, 2019
e69e7f0
Optimise room creation when students access
flxffy Jul 5, 2019
a9b44d5
Changed when rooms are created
flxffy Jul 5, 2019
d864c68
Refactored room.ex
flxffy Jul 5, 2019
a996615
Else clause
flxffy Jul 5, 2019
b3ba7a4
Grading controller
flxffy Jul 5, 2019
78c41a9
Fixed tests
flxffy Jul 5, 2019
19474a9
Script only for submitted submissions
flxffy Jul 5, 2019
ff5fbcf
Room creation logging
flxffy Jul 6, 2019
e709d0c
Tests for room creation
flxffy Jul 6, 2019
3dc73c2
Merge branch 'master' of https://github.com/source-academy/cadet
flxffy Jul 6, 2019
d5c1fdf
Changed db query in ChatkitRoom
flxffy Jul 6, 2019
aa75e3f
Minor fix
flxffy Jul 6, 2019
21de4ea
Minor fix to cassettes
flxffy Jul 6, 2019
05b830d
Fixed tests
flxffy Jul 6, 2019
842e534
Minor fix
flxffy Jul 6, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ $ vim config/secrets.exs
`luminus_redirect_url` are required for the application to properly authenticate with LumiNUS.
- A valid `cs1101s_repository`, `cs1101s_rsa_key` is required for the application to
run with the `--updater` flag. Otherwise, the default values will suffice.
- A valid `instance_id`, `key_id` and `key_secret` are required to use ChatKit's services. Otherwise, the placeholder values can be left as they are.

2. Install Elixir dependencies
```bash
Expand Down Expand Up @@ -66,6 +67,14 @@ We recommend setting up nginx to handle preflight checks using the following

If you do this, do remember to point cadet-frontend to port `4001` instead of `4000`

### Chatkit

The chat functionality replacing the previous comment field found in assignments is built on top of Chatkit. Its documentation can be found [here](https://pusher.com/docs/chatkit).

If you are using Chatkit, obtain your instance ID, key ID and secret key from your account, and set them in. Instructions to that are found [here](https://pusher.com/docs/chatkit/authentication#chatkit-key-and-instance-id).

Internet connection is required for usage.


### Style Guide

Expand Down
2 changes: 2 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ config :cadet, Cadet.Jobs.Scheduler,
overlap: false,
jobs: [
{"@hourly", {Mix.Tasks.Cadet.Assessments.Update, :run, [nil]}},
# Create Chatkit rooms if they do not already exist at 1am
{"0 1 * * *", {Mix.Tasks.Cadet.ChatkitRoom, :run, [nil]}},
# Grade previous day's submission at 3am
{"0 3 * * *", {Cadet.Autograder.GradingJob, :grade_all_due_yesterday, []}}
]
Expand Down
5 changes: 5 additions & 0 deletions config/secrets.exs.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ config :cadet,
],
autograder: [
lambda_name: "autograderLambdaName"
],
chat: [
instance_id: "CHATKIT_INSTANCE_ID",
key_id: "CHATKIT_KEY_ID",
key_secret: "CHATKIT_KEY_SECRET"
]

config :sentry,
Expand Down
10 changes: 8 additions & 2 deletions lib/cadet/assessments/answer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ defmodule Cadet.Assessments.Answer do
end

@required_fields ~w(answer submission_id question_id type)a
@optional_fields ~w(xp xp_adjustment grade comment adjustment grader_id)a
@optional_fields ~w(xp xp_adjustment grade adjustment grader_id)a

def changeset(answer, params) do
answer
Expand All @@ -49,7 +49,7 @@ defmodule Cadet.Assessments.Answer do
answer
|> cast(
params,
~w(grader_id xp xp_adjustment grade adjustment autograding_results autograding_status comment)a
~w(grader_id xp xp_adjustment grade adjustment autograding_results autograding_status)a
)
|> add_belongs_to_id_from_model(:grader, params)
|> foreign_key_constraint(:grader_id)
Expand All @@ -63,6 +63,12 @@ defmodule Cadet.Assessments.Answer do
|> validate_xp_grade_adjustment_total()
end

# TODO: add some validation
@spec comment_changeset(%__MODULE__{} | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
def comment_changeset(answer, params) do
cast(answer, params, ~w(comment)a)
end

@spec validate_xp_grade_adjustment_total(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp validate_xp_grade_adjustment_total(changeset) do
answer = apply_changes(changeset)
Expand Down
9 changes: 7 additions & 2 deletions lib/cadet/assessments/assessments.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, can this be placed where a submission occurs?

Copy link
Contributor Author

@flxffy flxffy Jul 6, 2019

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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"}}
Expand Down Expand Up @@ -468,7 +474,6 @@ defmodule Cadet.Assessments do
xp_adjustment: 0,
autograding_status: :none,
autograding_results: [],
comment: nil,
grader_id: nil
})
|> Repo.update()}
Expand Down
91 changes: 91 additions & 0 deletions lib/cadet/chat/room.ex
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
50 changes: 50 additions & 0 deletions lib/cadet/chat/token.ex
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
51 changes: 51 additions & 0 deletions lib/cadet_web/controllers/chat_controller.ex
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
2 changes: 2 additions & 0 deletions lib/cadet_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ defmodule CadetWeb.Router do
post("/grading/:submissionid/:questionid", GradingController, :update)

get("/user", UserController, :index)

post("/chat/token", ChatController, :index)
end

# Other scopes may use custom stacks.
Expand Down
10 changes: 10 additions & 0 deletions lib/cadet_web/views/chat_view.ex
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
34 changes: 34 additions & 0 deletions lib/mix/tasks/chatkit_room.ex
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
Loading