Skip to content

Instantly share code, notes, and snippets.

@adri326
Last active October 7, 2021 18:54
Show Gist options
  • Save adri326/8566e4fedd209b7a05647ee0d016a2e6 to your computer and use it in GitHub Desktop.
Save adri326/8566e4fedd209b7a05647ee0d016a2e6 to your computer and use it in GitHub Desktop.
WIP Unofficial Agora Quiz API

Unofficial Agora Quiz API

This API is used by Agora Quiz, a french, online platform where students submit questions about a course and challenge each other at answering them. The website has two account kinds: student and teachers. Teachers can create "groups" and share a code for their student to sign up or join that group.

This documentation presents our results at reverse-engineering the internal API used by the website. It is very incomplete and currently only focuses on the student's half of the website. Values marked with question marks indicate unknown behavior/values.

The API seems to have been written by french-speaking people, and while it is in english, some of the words use the french orthograph (eg. groupe instead of group).

Token

You will need an account on the website to be able to log in.

POST https://agora-quiz.education/api/sign_in
-> {
  username: "USERNAME",
  password: "HASHED_PASSWORD"
}
<- User (full)

The server will respond with a JSON file, with a Set-Cookie HTTP header containing your token (named connect.sid). The response contains an extended version of the User object, containing the hashed password.

The user's password must be hashed with a plain md5sum to obtain HASHED_PASSWORD.

All of the following endpoints need to have the connect.sid cookie transmitted. The other cookies are analytics-related and can be ignored.

To log back out, send a GET request to /api/logout:

GET https://agora-quiz.education/api/logout
<- {
  "message": "disconnected"
}

User endpoints

/api/me

GET https://agora-quiz.education/api/me
<- {
  "user": User,
  "counter": {
    "playing_game": int,
    "question_rejected": int,
    "question_unvalid": ?
  }
}

Returns your User object and a set of counters for the currenctly active games, likely used for the status bubbles on the "Jouer" and "Questions" buttons.

The current ID_GROUP, required for most api calls, can be found in the returned User object.

/api/users/ID_USER

PUT https://agora-quiz.education/api/users/ID_USER
-> {
  admin: bool,
  avatar_json: string,
  createdAt: Date,
  current_groupe: Group,
  current_groupe_id: int|null,
  defeats: int,
  equalities: int,
  finished_goals: int,
  good_answers_count: int,
  groups: [Group],
  id_user: int,
  is_establishment: bool,
  last_donation_alter: Date, // ?
  lastLogin: Date,
  new_mail: null|string,
  new_password: null|string,
  new_username: string,
  password: string,
  questions_added: int,
  questions_sent: int,
  score_user: int,
  updatedAt: Date,
  username: string,
  victories: int,
}

Group endpoints

/api/groupe/ID_GROUP/users/ID_USER/playing_games

GET https://agora-quiz.education/api/groupe/ID_GROUP/users/ID_USER/playing_games
<- {
  "count": 0,
  "rows": [Round]
}

Returns a list of the currently-played games (Round) by the given ID_USER in the group ID_GROUP. ID_USER can be any user that is member of the group.

Group endpoints

/api/groupe/ID_GROUP/count_playable_themes

GET https://agora-quiz.education/api/groupe/ID_GROUP/count_playable_themes
<- {
  "count": int
}

Returns how many themes can be selected when playing rounds.

/api/groupe/ID_GROUP/get_users_short_list

GET https://agora-quiz.education/api/groupe/ID_GROUP/get_users_short_list
<- {
  "count": int, // The number of users in the group (?), admins may not count towards that number
  "rows": [User]
}

Returns a list of the users in the group ID_GROUP. The users from that list have the fields id_user, username, avatar_json, admin, score_user, questions_added, users_groupe

/api/groupe/ID_GROUP/notifications

GET https://agora-quiz.education/api/groupe/ID_GROUP/notifications
<- {
  "count_new": int, // How many new notifications there are
  "rows": [Notification] // (?)
}

/api/groupe/ID_GROUP/top_three

GET https://agora-quiz.education/api/groupe/ID_GROUP/top_three
<- {
  "count": int, // The number of users in the group (?), higher than that of get_users_short_list, so maybe admins count towards that number
  "rows": [User] //
}

/api/groupe/ID_GROUP/three_random_playable_themes

GET https://agora-quiz.education/api/groupe/ID_GROUP/three_random_playable_themes
<- [Theme]

/api/groupe/ID_GROUP/games/ID_GAME

GET https://agora-quiz.education/api/groupe/ID_GROUP/games/ID_GAME
<- Game {
  id_game: string,
  score_1: int,
  score_2: int,
  round: int,
  is_playing: bool,
  is_finished: bool,
  is_cancelled: bool,
  is_friendly: bool,
  createdAt: Date,
  updatedAt: Date,
  groupe_id: int,
  user_id_1: int,
  user_id_2: int,
  current_player: int,
  user_1: User {
    id_user: int,
    username: string,
    avatar_json: string,
    admin: bool,
    score_user: int,
    questions_added: int,
    users_groupes: [ShortUser],
    groups: [Group],
  },
  user_2: User { /* Same as user_1 */ },
  curr_player: {username: string},
}

To enter a game, the website uses a PUT request on that endpoint:

PUT https://agora-quiz.education/api/groupe/ID_GROUP/games/ID_GAME
-> {
  groupe_id: int,
  id_game: string,
  is_playing: true
}
<- (same)

/api/ingame_questions

GET https://agora-quiz.education/api/ingame_questions/user/ID_USER/groupe/ID_GROUP/theme/THEME/order/ORDER_KEY/ORDER_BY/item_per_page/ITEMS/page/PAGE
-> {
  ID_USER: int, // ID of the user
  ID_GROUP: int, // ID of the group
  THEME: int|"all", // ID of the theme or "all" for every theme
  ORDER_KEY: "updatedAt"|"value_question"|"good_rep", // key of the question object -- potential injection?
  ORDER_BY: "ASC"|"DESC", // ASCendant or DESCendant
  ITEMS: int, // number of items per page
  PAGE: int, // page number (begins at 1)
}
<- {
  count: int,
  rows: [Question (full)]
}

This endpoint can only be accessed when ID_USER is the ID of the current user (for students).

Game endpoints

/api/groupe/GROUP_ID/users_answers

POST https://agora-quiz.education/api/groupe/GROUP_ID/users_answers
-> {
  answer_time: float,
  groupe_id: int,
  question_id: int,
  response_id: int,
  response_is_true: bool,
  round_id: int,
  theme_id: int,
  user_id: int
}
<- {
  "message": "users_answer has been succesfully added" // (sic) note the typo on succesfully
}

Sends an answer. Requires all fields to be set to correct values and requires a PUT request with the updated scores/status to be sent to /api/groupe/GROUP_ID/games/GAME_ID.

Objects

User

The User object contains public information about each user:

{
  "id_user": int, // The ID of that user, commonly referred to as ID_USER in this document

  "username": string, // The username of that user
  "admin": bool,
  "is_establishment": bool, // (?)
  "avatar_json": string, // Stringified JSON containing an Avatar object
  "current_groupe_id": int, // (sic) The ID of the currently-selected group
  "mail": string, // can be null
  "lastLogin": Date,
  "updatedAt": Date, // (?)
  "createdAt": Date,

  "score_user": int, // The current score of the user
  "victories": int, // The number of rounds won by the user
  "equalities": int, // (sic) The number of rounds drawn by the user
  "defeats": int, // The number of rounds lost by the user
  "questions_added": int, // The number of questions added by the user that got approved
  "questions_sent": int, // The number of questions proposed by the user
  "answer_count": int, // The number of questions answered by the user
  "good_answer_count": int, // The number of correct answers given by the user
  "finished_goals": int, // (?)

  "last_donation_alter": string, // (?)
  "current_groupe": Group, // The Group with id `current_groupe_id`
  "groups": [Group], // The list of groups that the user belongs to; seems to contain an alternative set of proprieties
  "users_groupes": [User], // (sic) (?) Sometimes set, only contains statistics
  "users_groupe": User, // (sic) (?) Sometimes set, does not contain users_groupe. When set, contains the statistics of that user corresponding to the queried group *and a different ID* (make a new object?)
}

Group

The Group object contains public information about a group (aka a class or course, created by a teacher):

{
  "id_groupe": int, // The ID of the group

  "level": string, // The graduate/undergraduate level of the corresponding course, in text representation (french form only?)
  "topic": string, // The topic of the corresponding course, can be set to "Autre" for "other"
  "name_groupe": string, // (sic) the name of the group
  "code_groupe": int, // (sic) 6-digit code used by students to join the group
  "password_groupe": string, // (sic) the password, required to join the group
  "description_groupe": ?, // (sic) can be null
  "user_id": int, // ID of the user who created the group (?)
  "color_groupe_1": int, // (sic) (?) can be null
  "color_groupe_2": int, // (sic) (?) can be null

  "timeToAnswer": int, // The maximum time that a student can take to answer a question, in seconds
  "showGoodRep": bool, // (?)
  "showOpponentAnswer": bool, // (?) whether or not users can see their opponent's answers at the end of a round, if false, only the correctness is displayed
  "showLadder": int, // (?) The number of places to show on the leaderboard
  "showLadderGoodAnswers": bool, // (?)
  "showGroupStats": bool, // (sic) (?)
  "restrictGroupNews": bool, // (sic) (?)
  "helpMathFormula": bool, // (?)
  "allowAddImage": bool, // (?)

  "users_groupes": [ShortUser], // (sic) (?)
  "admin_user": User, // (sic) (?) Sometimes not set, but when set it only contains `username`, `avatar_json`, `admin` and `users_groupes.score_user`
  "users_groupe": ?, // (sic) (?) Sometimes set to a User instance
}

ShortUser

This object is a shorter representation of a user, used within Group.

{
  "score_user": int, // The score of that user
  "victories": int, // The number of rounds won by that user
  "equalities": int, // (sic) The number of rounds drawn by the user
  "defeats": int, // The number of rounds lost by the user
  "questions_added": int, // The number of questions added by the user that got approved
  "questions_sent": int, // The number of questions proposed by the user
  "answer_count": int, // The number of questions answered by the user
  "good_answer_count": int, // The number of correct answers given by the user
  "finished_goals": int, // (?)
  "lastActivity": Date, // (?)
  "updatedAt": Date // (?)

Avatar

The avatar is edited in a small editor and lets users personalize their profile picture. It is made up of three layers: the background head, the eyes and the mouth. This structure is usually manipulated in string format.

{
  "form": {
    "pattern": int, // The shape of the head
    "colors": int // The background color of the head
  },
  "eye": {
    "pattern": int, // The shape of the eyes
    "colors": int // The color of the eyes
  },
  "mouth": {
    "pattern": int, // The shape of the mouth
    "colors": int // The color of the mouth
  }
}

Theme

{
  "id_theme": int,
  "name_theme": string, // Name of the theme
  "color_theme": int, // (?)
  "groupe_theme": {
    "is_playable": bool,
  }
}

Date

A Date can either be in its ISO string representation (yyyy-mm-ddThh:mm:ss.sssZ). The server can also return an object, for special values. Values encoutered so far are:

  • {val: "CURRENT_TIMESTAMP"}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment