14Degrees, a distributed social networking app designed to help you find connections within 14 degrees of separation.
- Remote Connections
- New features developed since Project Presentation
- Local Setup -
- API Documentation
- Planning & Design
Teams that we connected to:
- Team 10 - https://socioecon.herokuapp.com/home/
- Team 11 - https://cmsjmnet.herokuapp.com/
- Team 14 - https://social-distribution-14degrees2.herokuapp.com/
If you would like to use a local Postgres DB instead of the default SQLite DB you need to do the following:
- Have PostgreSQL server installed on your local machine
- Enter into the PSQL console with
sudo -u postgres psql
- In the opened PSQL console run the following commands
CREATE DATABASE social_distribution_14; CREATE USER team_14 WITH PASSWORD 'team14'; ALTER USER team_14 WITH SUPERUSER;
- You also need to set an environment variable to use Postgres -
export LOCAL_POSTGRESQL=true
- Move into the backend directory of the repository:
cd backend
- Create an start a virtual environment:
python3 -m venv env
source env/bin/activate # On Windows use `env\Scripts\activate`
- Install dependencies
pip install -r requirements.txt
-
cd backend && python manage.py migrate
-
Run the webserver
python manage.py runserver
- You should be able to run the server on a non-default port by running something like
python manage.py runserver 5900
When asked for authentication enter any of the authors usernames with the password pass123
python manage.py flush (if your database is not empty)
python manage.py loaddata fixtures/all_data.json
- At first, you can create any set of data of your liking through the list API routes or though the frontend.
- Once you have inserted some meaningful data into your local database, you can dump the data into file fixtures. To do that make sure that you are in the Django project directory (
backend/
) and run the following commands -
python manage.py dumpdata webserver --indent 4 > mydata.json
This will create a file called mydata.json
in your current working directory.
- If you want, you can push this dataset in the
webserver/fixtures
directory of our Github repository. - To load this dataset to your local db at a later time, run the following commands -
python manage.py flush # this will clear the current data in your db
python manage.py loaddata mydata.json
Now, you're all set!
Resource | POST | GET | PUT | DELETE |
---|---|---|---|---|
/api/login/ | Logs in an author | - | - | - |
/api/register/ | Registers a new author | - | - | - |
/api/logout/ | Logs out an author | - | - | - |
/api/posts/ | - | Retrieves the list of public posts on the server (open to everyone) | - | - |
/api/authors/ | - | Retrieves the list of authors [A][R] | - | - |
/api/authors/<author_id>/ | Updates an author's profile [A] | Retrieves an author's profile [A][R] | - | - |
/api/authors/<author_id>/inbox/ | Creates a new inbox item for an author [A][R] | Retrieve's an author's inbox [A] | - | - |
/api/authors/<author_id>/follow-requests/ | - | Retrives the list of follow requests for an author [A] | - | - |
/api/authors/<author_id>/follow-requests/<foreign_author_id>/ | - | - | - | Decline a follow request[A] |
/api/authors/<author_id>/followers/ | - | Retrives the list of followers for an author [A][R] | - | - |
/api/authors/<author_id>/followers/<foreign_author_id>/ | - | Checks if foreign_author_id is a follower of author_id [A] | Accepts a follow request [A] | Removes a follower [A] |
/api/authors/<author_id>/posts/ | Creates a new post for an author [A] | Retrieves recent posts from an author [A][R] | - | - |
/api/authors/<author_id>/posts/<post_id>/ | Update an authors post [A] | Retrieves an authors post [A][R] | - | Delete an authors post [A] |
/api/nodes/ | Add a node [Admin only] | - | - | - |
/api/authors/<author_id>/posts/<post_id>/likes/ | - | Retrieves a list of likes on an authors post [A][R] | - | - |
/api/authors/<author_id>/liked/ | - | Retrieves a list of public things liked by an author [A][R] | - | - |
/api/authors/<author_id>/posts/<post_id>/image/ | - | Retrieves an image [R] | - | - |
/api/authors/<author_id>/posts/<post_id>/comments/ | - | Retrieves the comments for a post [A][R] | - | - |
/api/authors/<author_id>/posts/<post_id>/comments/<comment_id>/likes/ | - | Retrieves the list of likes made on a comment [A][R] | - | - |
- [R] specifies that a remote request can be made to the route. In other words, only those routes marked with [R] accept remote requests. They have also been bolded for ease of navigability.
- [A] specifies that the request must be authenticated
- [WIP] specifies that some features of the endpoint is under development
username
password
password2
display_name
{
"username": "author532",
"display_name": "good_author"
}
201 Created
400 Bad Request
username
password
{
"message": "Login Success"
}
200 OK
400 Bad Request
401 Unauthorized
None
{
"message": "Successfully Logged out"
}
200 OK
[
{
"url": "http://127.0.0.1:8000/api/authors/6e3c2a39-8fef-4efb-bb98-0826a7f15f39/",
"id": "6e3c2a39-8fef-4efb-bb98-0826a7f15f39",
"display_name": "myuser",
"profile_image": "",
"github_handle": ""
},
{
"url": "http://127.0.0.1:8000/api/authors/255c89fd-1b47-4f42-8a1b-5c574c6117f3/",
"id": "255c89fd-1b47-4f42-8a1b-5c574c6117f3",
"display_name": "author_1",
"profile_image": "",
"github_handle": ""
}
]
200 OK
401 Unauthorized
{
"url": "http://127.0.0.1:8000/api/authors/6e3c2a39-8fef-4efb-bb98-0826a7f15f39/",
"id": "6e3c2a39-8fef-4efb-bb98-0826a7f15f39",
"display_name": "myuser",
"profile_image": "",
"github_handle": ""
}
200 OK
401 Unauthorized
404 Not Found
{
"url": "http://127.0.0.1:8000/api/authors/6e3c2a39-8fef-4efb-bb98-0826a7f15f39/",
"id": "6e3c2a39-8fef-4efb-bb98-0826a7f15f39",
"display_name": "noob_author",
"profile_image": "",
"github_handle": ""
}
200 OK
401 Unauthorized
404 Not Found
Note: You may wonder why the author url
s are needed with the request payload. This is needed for project part 2 to differentiate between a local author and a remote one. If an author is a remote one, we will forward the follow request to it's matching remote server.
{
"message": "OK"
}
201 Created
400 Bad Request
401 Unauthorized
404 Not Found
409 Conflict
: This can come up when you try to re-send a follow request when the request is still pending
[
{
"url": "http://127.0.0.1:8000/api/authors/6e3c2a39-8fef-4efb-bb98-0826a7f15f39/",
"id": "6e3c2a39-8fef-4efb-bb98-0826a7f15f39",
"display_name": "noob_author",
"profile_image": "",
"github_handle": ""
},
{
"url": "http://127.0.0.1:8000/api/authors/edcfedc2-0c39-40e9-94de-7d234ebf408e/",
"id": "edcfedc2-0c39-40e9-94de-7d234ebf408e",
"display_name": "author_Z",
"profile_image": "",
"github_handle": ""
}
]
200 OK
401 Unauthorized
404 Not Found
Author with id 255c89fd-1b47-4f42-8a1b-5c574c6117f3 accepts a follow request of author with id 6e3c2a39-8fef-4efb-bb98-0826a7f15f39 -
{
"message": "OK"
}
201 Created
400 Bad Request
: This can be returned under a few different circumstances - when a matching follow request does not exist, when valid request payload is not given, or when an authors try to follow themselves401 Unauthorized
404 Not Found
{
"message": "Follow request declined"
}
200 OK
: means follow request was declined400 Bad Request
401 Unauthorized
404 Not Found
: can be returned when a matching follow request does not exist
{
"message": "follower removed"
}
200 OK
400 Bad Request
401 Unauthorized
404 Not Found
: this can be returned when a matching follower can't be found
{
"message": "edcfedc2-0c39-40e9-94de-7d234ebf408e is not a follower of 255c89fd-1b47-4f42-8a1b-5c574c6117f3"
}
Note: We can update the response messages over time if requested.
200 OK
: this is returned when a matching follower specified by the resource url was found.401 Unauthorized
404 Not Found
: this is returned when a matching follower is not found
Retrieve the list of followers for author_id 255c89fd-1b47-4f42-8a1b-5c574c6117f3 -
[
{
"url": "http://127.0.0.1:8000/api/authors/6e3c2a39-8fef-4efb-bb98-0826a7f15f39/",
"id": "6e3c2a39-8fef-4efb-bb98-0826a7f15f39",
"display_name": "noob_author",
"profile_image": "",
"github_handle": ""
}
]
200 OK
401 Unauthorized
404 Not Found
Retrieve the inbox of author with id 255c89fd-1b47-4f42-8a1b-5c574c6117f3-
[
{
"sender": {
"url": "http://127.0.0.1:8000/api/authors/edcfedc2-0c39-40e9-94de-7d234ebf408e/",
"id": "edcfedc2-0c39-40e9-94de-7d234ebf408e",
"display_name": "noob_author",
"profile_image": "",
"github_handle": ""
},
"type": "follow"
},
{
"author": {
"url": "http://127.0.0.1:8000/api/authors/edcfedc2-0c39-40e9-94de-7d234ebf408e/",,
"id": "edcfedc2-0c39-40e9-94de-7d234ebf408e",
"display_name": "noob_author",
"profile_image": "",
"github_handle": ""
},
"created_at": "2022-10-23T03:25:09.184425Z",
"edited_at": null,
"title": "Post 1",
"description": "Sample description",
"source": "",
"origin": "",
"unlisted": false,
"content_type": "text/plain",
"content": "What's up people?",
"visibility": "FRIENDS",
"type": "post"
}
]
Notes:
- The
type
field will be present in each inbox item. This specifies the type of the inbox. Possible values:"post"
for a new post,"follow"
for a follow request
200 OK
401 Unauthorized
404 Not Found
Retrieve the list of posts for author id 255c89fd-1b47-4f42-8a1b-5c574c6117f3 -
[
{
"author": {
"url": "http://localhost:8000/api/authors/255c89fd-1b47-4f42-8a1b-5c574c6117f3/",
"id": 255c89fd-1b47-4f42-8a1b-5c574c6117f3,
"display_name": "myuser",
"profile_image": "",
"github_handle": ""
},
"created_at": "2022-10-22T05:06:49.477100Z",
"edited_at": "2022-10-22T23:27:41.589319Z",
"title": "my first post!",
"description": "Hello world",
"source": "",
"origin": "",
"unlisted": false,
"content_type": "text/plain",
"content": "change the content",
"visibility": "PUBLIC",
"likes_count": 2
}
]
200 OK
401 Unauthorized
404 Not Found
Author with id 255c89fd-1b47-4f42-8a1b-5c574c6117f3 creates a new post-
[
{
"id": "824fbe15-4e6b-42b0-8bce-eadfc2914f26",
"title": "My first post",
"description": "My first post",
"unlisted": false,
"content": "some content",
"visibility": "PUBLIC",
"content_type": "text/plain"
}
]
201 Created
400 Bad Request
401 Unauthorized
404 Not Found
Retrieve the post for author id 255c89fd-1b47-4f42-8a1b-5c574c6117f3 with post id 824fbe15-4e6b-42b0-8bce-eadfc2914f26 -
[
{
"id": "824fbe15-4e6b-42b0-8bce-eadfc2914f26",
"author": {
"url": "http://127.0.0.1:8000/api/authors/255c89fd-1b47-4f42-8a1b-5c574c6117f3/",
"id": "255c89fd-1b47-4f42-8a1b-5c574c6117f3",
"display_name": "author_1",
"profile_image": "",
"github_handle": ""
},
"created_at": "2022-11-11T22:39:45.407227Z",
"edited_at": null,
"title": "My first post",
"description": "My first post",
"source": "",
"origin": "",
"unlisted": false,
"content_type": "text/plain",
"content": "some content",
"visibility": "PUBLIC",
"likes_count": 0
}
]
200 OK
401 Unauthorized
404 Not Found
400 Bad Request
Update the post for author id 255c89fd-1b47-4f42-8a1b-5c574c6117f3 with post id 824fbe15-4e6b-42b0-8bce-eadfc2914f26 -
[
{
"id": "824fbe15-4e6b-42b0-8bce-eadfc2914f26",
"title": "update my post title",
"description": "update my post description",
"unlisted": false,
"content": "some updated content"
}
]
200 OK
401 Unauthorized
404 Not Found
400 Bad Request
Delete the post for author id 255c89fd-1b47-4f42-8a1b-5c574c6117f3 with post id 824fbe15-4e6b-42b0-8bce-eadfc2914f26-
[
{
"message": "Object deleted!"
}
]
200 OK
401 Unauthorized
404 Not Found
400 Bad Request
Retreive a list of likes on author with id 255c89fd-1b47-4f42-8a1b-5c574c6117f3 post with post id 9b050b09-97d1-44b7-89ec-d2ed2c23cde1 -
[
{
"author": {
"url": "http://127.0.0.1:8000/api/authors/6e3c2a39-8fef-4efb-bb98-0826a7f15f39/",
"id": "6e3c2a39-8fef-4efb-bb98-0826a7f15f39",
"display_name": "cjenkins123",
"profile_image": "",
"github_handle": "cashj45"
},
"post": "http://127.0.0.1:8000/api/authors/6e3c2a39-8fef-4efb-bb98-0826a7f15f39/posts/9b050b09-97d1-44b7-89ec-d2ed2c23cde1/",
"object": "http://127.0.0.1:8000/api/authors/6e3c2a39-8fef-4efb-bb98-0826a7f15f39/posts/9b050b09-97d1-44b7-89ec-d2ed2c23cde1/"
},
{
"author": {
"url": "http://127.0.0.1:8000/api/authors/edcfedc2-0c39-40e9-94de-7d234ebf408e/",
"id": "edcfedc2-0c39-40e9-94de-7d234ebf408e",
"display_name": "UltimateBeast123",
"profile_image": "",
"github_handle": "ultimateBeast"
},
"post": "http://127.0.0.1:8000/api/authors/edcfedc2-0c39-40e9-94de-7d234ebf408e/posts/9b050b09-97d1-44b7-89ec-d2ed2c23cde1/",
"object": "http://127.0.0.1:8000/api/authors/edcfedc2-0c39-40e9-94de-7d234ebf408e/posts/9b050b09-97d1-44b7-89ec-d2ed2c23cde1/"
}
]
- The
author
represents the author who liked the post
200 OK
401 Unauthorized
404 Not Found
[
{
"author": {
"url": "http://127.0.0.1:8014/api/authors/2cd3cfe1-c56d-45aa-8640-79ffa40f7e3d/",
"id": "2cd3cfe1-c56d-45aa-8640-79ffa40f7e3d",
"display_name": "john",
"profile_image": "",
"github_handle": ""
},
"comment": "http://127.0.0.1:8014/api/authors/30558702-f634-4b24-bfa6-4bd25ab441c5/posts/595b9f1f-0053-4f44-ad88-aac59f2a6da2/comments/afd4bd59-188a-4100-9599-37b26f5e50c8/",
"object": "http://127.0.0.1:8014/api/authors/30558702-f634-4b24-bfa6-4bd25ab441c5/posts/595b9f1f-0053-4f44-ad88-aac59f2a6da2/comments/afd4bd59-188a-4100-9599-37b26f5e50c8/"
},
{
"author": {
"url": "http://127.0.0.1:8014/api/authors/2cd3cfe1-c56d-45aa-8640-79ffa40f7e3d/",
"id": "2cd3cfe1-c56d-45aa-8640-79ffa40f7e3d",
"display_name": "john",
"profile_image": "",
"github_handle": ""
},
"post": "http://127.0.0.1:8014/api/authors/2cd3cfe1-c56d-45aa-8640-79ffa40f7e3d/posts/595b9f1f-0053-4f44-ad88-aac59f2a6da2/",
"object": "http://127.0.0.1:8014/api/authors/2cd3cfe1-c56d-45aa-8640-79ffa40f7e3d/posts/595b9f1f-0053-4f44-ad88-aac59f2a6da2/"
}
]
200 OK
401 Unauthorized
404 Not Found
Author with id 442352d0-f10a-4ac9-a42b-55c2f41179b3 likes post 9b050b09-97d1-44b7-89ec-d2ed2c23cde1 made by author with id 255c89fd-1b47-4f42-8a1b-5c574c6117f3
{
"message": "OK"
}
201 Created
400 Bad Request
401 Unauthorized
404 Not Found
- Send the request at the inbox route of the comment's owner
- The
author
field represents the author liking the post
201 Created
400 Bad Request
401 Unauthorized
404 Not Found
Same as updating other types of inboxes.
201 Created
400 Bad Request
401 Unauthorized
404 Not Found
Understand how image posts are meant to be used from this eclass forum post.
- Uses the regular post creation route
- Required payload fields -
content_type
: specify either "image/jpeg;base64" or "image/png;base64"image
: enter the base64 encoded image string
- Optional payload fields (just here to satisfy requirements) -
unlisted
: default value istrue
so that image posts do not show up on the feed by themselves (if they did, then the base64 encoded strings would show up in thecontent
field of the post).visibility
: image posts are "PUBLIC" by default so that they are globally accessible by their url. I would keep the visibility to public for all image posts so that remote nodes can also fetch them to render images in their corresponding markdown.
{
"id": "f5a85964-3dc8-4d30-84da-17bac1f7f5fe",
"url": "http://127.0.0.1:8014/api/authors/c134c50a-76d7-498e-b55e-b7cff72936db/posts/f5a85964-3dc8-4d30-84da-17bac1f7f5fe/image/"
}
You will be able to access the image at the url specified -
You can use the provided url to set the value of the src
attribute of an <img>
tag to embed an image within a post.
Same as a regular post creation.
Send a GET request to a url such as http://127.0.0.1:8014/api/authors/c134c50a-76d7-498e-b55e-b7cff72936db/posts/f5a85964-3dc8-4d30-84da-17bac1f7f5fe/image/
- The
post_id
provided has to be the ID of the image post
200 OK
404 Not Found
400 Bad Request
- this can be returned if you are unauthorized to view the image
- Use the inbox route of the post's author to create a comment
- The top-level
author
field represents the author who is making the comment - The nested
author
field represents the author of the post content_type
is optional and has a default value of "text/plain"
{
"message": "OK"
}
201 Created
400 Bad Request
404 Not Found
Additionally, here's how the comment inbox would look like if you send a GET request to the author's inbox -
[
{
"author": {
"url": "http://127.0.0.1:8014/api/authors/ee87c0c2-2eda-4071-859e-8aeff3638231/",
"id": "ee87c0c2-2eda-4071-859e-8aeff3638231",
"display_name": "hugh",
"profile_image": "",
"github_handle": ""
},
"comment": "Awesome post!",
"content_type": "text/plain",
"created_at": "2022-12-04T06:38:13.906331Z",
"id": "http://127.0.0.1:8014/api/authors/30558702-f634-4b24-bfa6-4bd25ab441c5/posts/595b9f1f-0053-4f44-ad88-aac59f2a6da2/comments/afd4bd59-188a-4100-9599-37b26f5e50c8/",
"type": "comment"
}
]
If the post is not PUBLIC, only the post's author can fetch comments from this route.
[
{
"author": {
"url": "http://127.0.0.1:8014/api/authors/ee87c0c2-2eda-4071-859e-8aeff3638231/",
"id": "ee87c0c2-2eda-4071-859e-8aeff3638231",
"display_name": "hugh",
"profile_image": "",
"github_handle": ""
},
"comment": "Awesome post!",
"content_type": "text/plain",
"created_at": "2022-12-04T06:38:13.906331Z",
"id": "http://127.0.0.1:8014/api/authors/30558702-f634-4b24-bfa6-4bd25ab441c5/posts/595b9f1f-0053-4f44-ad88-aac59f2a6da2/comments/afd4bd59-188a-4100-9599-37b26f5e50c8/"
}
]
200 Ok
401 Unauthorized
403 Forbidden
- this returned when you are authenticated but not allowed to get comments from this post404 Not Found
Comments will also be associated with their corresponding posts in the following format -
[
{
"id": "595b9f1f-0053-4f44-ad88-aac59f2a6da2",
"author": {
"url": "http://127.0.0.1:8014/api/authors/30558702-f634-4b24-bfa6-4bd25ab441c5/",
"id": "30558702-f634-4b24-bfa6-4bd25ab441c5",
"display_name": "cashJenkins",
"profile_image": "",
"github_handle": ""
},
"created_at": "2022-12-04T06:33:41.909944Z",
"edited_at": null,
"title": "Test post",
"description": "Test description",
"source": "",
"origin": "",
"unlisted": false,
"content_type": "text/plain",
"content": "Test content",
"visibility": "PUBLIC",
"likes_count": 0,
"count": 1,
"comments": "http://127.0.0.1:8014/api/authors/30558702-f634-4b24-bfa6-4bd25ab441c5/posts/595b9f1f-0053-4f44-ad88-aac59f2a6da2/comments",
"comments_src": {
"type": "comments",
"page": 1,
"size": 1,
"comments": [
{
"author": {
"url": "http://127.0.0.1:8014/api/authors/ee87c0c2-2eda-4071-859e-8aeff3638231/",
"id": "ee87c0c2-2eda-4071-859e-8aeff3638231",
"display_name": "hugh",
"profile_image": "",
"github_handle": ""
},
"comment": "Awesome post!",
"content_type": "text/plain",
"created_at": "2022-12-04T06:38:13.906331Z",
"id": "http://127.0.0.1:8014/api/authors/30558702-f634-4b24-bfa6-4bd25ab441c5/posts/595b9f1f-0053-4f44-ad88-aac59f2a6da2/comments/afd4bd59-188a-4100-9599-37b26f5e50c8/"
}
]
}
}
]
- Notice the newly added fields that are related to comments -
count
,comments
,comments_src
- At most 5 comments will be returned with a post (ordered by most recent comments first)
- Keep in mind that comments will not be returned with all posts
- Comments will always be returned with public posts
- If the post is not public, comments will only be returned if the request is made by the author of the post
{
"count": 27,
"next": "http://localhost:8000/api/authors/?page=2&size=5",
"previous": null,
"results": [
{
"url": "http://localhost:8000/api/authors6e3c2a39-8fef-4efb-bb98-0826a7f15f39/",
"id": "6e3c2a39-8fef-4efb-bb98-0826a7f15f39",
"display_name": "",
"profile_image": "",
"github_handle": ""
},
{
"url": "http://localhost:8000/api/authors/255c89fd-1b47-4f42-8a1b-5c574c6117f3/",
"id": "255c89fd-1b47-4f42-8a1b-5c574c6117f3",
"display_name": "zarif",
"profile_image": "",
"github_handle": ""
},
{
"url": "http://localhost:8000/api/authors/edcfedc2-0c39-40e9-94de-7d234ebf408e/",
"id": "edcfedc2-0c39-40e9-94de-7d234ebf408e",
"display_name": "author_0_handle",
"profile_image": "",
"github_handle": ""
},
{
"url": "http://localhost:8000/api/authors/442352d0-f10a-4ac9-a42b-55c2f41179b3/",
"id": "442352d0-f10a-4ac9-a42b-55c2f41179b3",
"display_name": "author_1_handle",
"profile_image": "",
"github_handle": ""
},
{
"url": "http://localhost:8000/api/authors/e6573c76-6916-45c9-af94-0d8130b8ec1f/",
"id": e6573c76-6916-45c9-af94-0d8130b8ec1f,
"display_name": "author_2_handle",
"profile_image": "",
"github_handle": ""
}
]
}
- Use the
page
query parameter to specify the page you would like to fetch. If you just specify thepage
query param without thesize
, a default size of 10 will be used - Use the
size
query parameter to specify the size of the page. You cannot use this on it's one (you also need to pass thepage
query parameter). count
specifies the total number of records available at this resource- Send requests to the urls specified in
next
andprev
to use the pagination
node_name
,password
,password2
are required authentication fields that will be used by external nodes to connect to usauth_username
,auth_password
are required authentication fields that will be used by our node to connect to external nodesapi_url
is base api url of the external node
- The external node will need to authenticate themselves with
username:node_name
andpassword:password
- You can view the ER model for our app here.
- You can view the Figma designs for our UI here.
If you are added as collaborator on the Heroku app for this project, you should be able to access it here - https://dashboard.heroku.com/apps/social-distribution-14degrees. You are able to do pretty much any administration work on the Heroku app once you're added as a collaborator. Some examples of what you can do -
- You can manually deploy any branch on this repository from the
Deploy
tab - You can ssh into the production container/dyno with the command
heroku ps:exec --dyno=web.1 --app social-distribution-14degrees
- You can ssh into a one-off (non-production) container/dyno with the command
heroku run bash -a social-distribution-14degrees
Send a pull request and be sure to update this file with your name.
Generally everything is LICENSE'D under the Apache 2 license by Zarif Mahfuz.
All text is licensed under the CC-BY-SA 4.0 http://creativecommons.org/licenses/by-sa/4.0/deed.en_US
Contributors:
Zarif Mahfuz
Mark McGoey
Chidubem Ogbudibe
Timothee Legros