├── apps ├── search_app │ ├── .dockerignore │ ├── src │ │ └── main │ │ │ └── resources │ │ │ ├── config.properties │ │ │ └── commandmap.properties │ ├── Dockerfile │ └── docker-compose.yml ├── thread_app │ ├── .dockerignore │ ├── src │ │ ├── main │ │ │ ├── resources │ │ │ │ ├── cacheThreshold.properties │ │ │ │ ├── configurations.properties │ │ │ │ ├── config.properties │ │ │ │ └── commandmap.properties │ │ │ └── java │ │ │ │ └── org │ │ │ │ └── sab │ │ │ │ └── thread │ │ │ │ └── commands │ │ │ │ └── GetFollowedThreads.java │ │ └── test │ │ │ └── java │ │ │ └── org │ │ │ └── sab │ │ │ └── thread │ │ │ └── ThreadAppTest.java │ ├── Dockerfile │ └── docker-compose.yml ├── user_app │ ├── .dockerignore │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ ├── config.properties │ │ │ └── commandmap.properties │ │ │ └── java │ │ │ └── org │ │ │ └── sab │ │ │ └── user │ │ │ ├── commands │ │ │ ├── ViewAnotherProfile.java │ │ │ ├── ViewMyProfile.java │ │ │ ├── Login.java │ │ │ ├── UpdatePassword.java │ │ │ ├── DeleteProfilePhoto.java │ │ │ └── UpdateProfilePhoto.java │ │ │ └── UserApp.java │ ├── Dockerfile │ └── docker-compose.yml ├── notification_app │ ├── .dockerignore │ ├── src │ │ ├── main │ │ │ ├── resources │ │ │ │ ├── config.properties │ │ │ │ └── commandmap.properties │ │ │ └── java │ │ │ │ └── org │ │ │ │ └── sab │ │ │ │ └── notification │ │ │ │ ├── NotificationSendingFailedException.java │ │ │ │ ├── GoogleCredentialsLoadingFailedException.java │ │ │ │ ├── NotificationApp.java │ │ │ │ ├── FirebaseInitializer.java │ │ │ │ └── commands │ │ │ │ └── GetNotifications.java │ │ └── test │ │ │ └── java │ │ │ └── org │ │ │ └── sab │ │ │ └── notification │ │ │ └── commands │ │ │ └── SendNotificationTest.java │ ├── .gitignore │ ├── docker-compose.yml │ └── Dockerfile ├── recommendation_app │ ├── .dockerignore │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ ├── config.properties │ │ │ ├── config.development.json │ │ │ └── commandmap.properties │ │ │ └── java │ │ │ └── org │ │ │ └── sab │ │ │ └── recommendation │ │ │ └── commands │ │ │ ├── GetPopularThreads.java │ │ │ └── GetPopularSubThreads.java │ ├── Dockerfile │ └── docker-compose.yml ├── subthread_app │ ├── .dockerignore │ ├── src │ │ ├── main │ │ │ ├── resources │ │ │ │ ├── configurations.properties │ │ │ │ ├── config.properties │ │ │ │ ├── cacheThreshold.properties │ │ │ │ └── commandmap.properties │ │ │ └── java │ │ │ │ └── org │ │ │ │ └── sab │ │ │ │ └── subthread │ │ │ │ └── commands │ │ │ │ ├── GetComment.java │ │ │ │ ├── GetSubThreads.java │ │ │ │ ├── GetMyComments.java │ │ │ │ ├── GetMyDislikedComments.java │ │ │ │ ├── GetMySubThreads.java │ │ │ │ ├── GetMyLikedComments.java │ │ │ │ ├── GetMyDislikedSubThreads.java │ │ │ │ ├── GetMyLikedSubThreads.java │ │ │ │ └── ModeratorSeeReports.java │ │ └── test │ │ │ └── java │ │ │ └── org │ │ │ └── sab │ │ │ └── subthread │ │ │ └── SubThreadAppTest.java │ ├── Dockerfile │ └── docker-compose.yml ├── chat_app │ ├── chat_server │ │ ├── .dockerignore │ │ ├── src │ │ │ ├── test │ │ │ │ └── java │ │ │ │ │ └── org │ │ │ │ │ └── sab │ │ │ │ │ └── chat │ │ │ │ │ └── server │ │ │ │ │ └── AppTest.java │ │ │ └── main │ │ │ │ └── java │ │ │ │ └── org │ │ │ │ └── sab │ │ │ │ └── chat │ │ │ │ └── server │ │ │ │ ├── handlers │ │ │ │ ├── ResponseHandler.java │ │ │ │ ├── TextWebSocketFrameHandler.java │ │ │ │ └── QueueHandler.java │ │ │ │ ├── routers │ │ │ │ ├── RouterBuilder.java │ │ │ │ ├── GetGroupMessagesRouter.java │ │ │ │ ├── GetChatsRouter.java │ │ │ │ ├── GetDirectMessagesRouter.java │ │ │ │ ├── CreateGroupMessageRouter.java │ │ │ │ ├── CreateDirectMessageRouter.java │ │ │ │ ├── AddMemberRouter.java │ │ │ │ ├── RemoveMemberRouter.java │ │ │ │ ├── CreateGroupChatRouter.java │ │ │ │ ├── LeaveGroupRouter.java │ │ │ │ └── CreateDirectChatRouter.java │ │ │ │ ├── ChatServerInitializer.java │ │ │ │ └── ChatServer.java │ │ └── Dockerfile │ ├── chat_storage │ │ ├── .dockerignore │ │ ├── src │ │ │ ├── main │ │ │ │ ├── resources │ │ │ │ │ ├── config.properties │ │ │ │ │ └── commandmap.properties │ │ │ │ └── java │ │ │ │ │ └── org │ │ │ │ │ └── sab │ │ │ │ │ └── chat │ │ │ │ │ └── storage │ │ │ │ │ ├── ChatStorageApp.java │ │ │ │ │ ├── exceptions │ │ │ │ │ └── InvalidInputException.java │ │ │ │ │ ├── config │ │ │ │ │ └── KeyspaceInitializer.java │ │ │ │ │ ├── tables │ │ │ │ │ └── TableUtils.java │ │ │ │ │ └── models │ │ │ │ │ └── DirectChat.java │ │ │ └── test │ │ │ │ └── java │ │ │ │ └── org │ │ │ │ └── sab │ │ │ │ └── chat │ │ │ │ └── storage │ │ │ │ ├── AppTest.java │ │ │ │ └── config │ │ │ │ └── CassandraConnectorTest.java │ │ └── Dockerfile │ ├── README.md │ └── docker-compose.yml ├── user_to_user_actions_app │ ├── .dockerignore │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ ├── config.properties │ │ │ └── commandmap.properties │ │ │ └── java │ │ │ └── org │ │ │ └── sab │ │ │ └── useractions │ │ │ └── UserToUserActionsApp.java │ ├── Dockerfile │ └── docker-compose.yml ├── nginx │ ├── Dockerfile │ └── nginx.conf ├── example_app │ └── src │ │ ├── main │ │ ├── resources │ │ │ ├── config.properties │ │ │ ├── ImprovedHello │ │ │ ├── MorningWorld │ │ │ └── commandmap.properties │ │ └── java │ │ │ └── org │ │ │ └── sab │ │ │ └── demo │ │ │ ├── commands │ │ │ ├── GoodByeWorld.java │ │ │ ├── HelloWorld.java │ │ │ ├── CommandWithoutAuth.java │ │ │ ├── CommandNeedingAuth.java │ │ │ ├── CommandNeedingClaims.java │ │ │ └── HelloArango.java │ │ │ └── ExampleApp.java │ │ └── test │ │ └── java │ │ └── org │ │ └── sab │ │ └── demo │ │ ├── CommandWithoutAuthTest.java │ │ ├── CommandNeedingAuthTest.java │ │ └── CommandNeedingClaimsTest.java └── controller │ └── src │ ├── main │ ├── resources │ │ ├── apps-ports.properties │ │ └── apps-ips.properties │ └── java │ │ └── org │ │ └── sab │ │ └── controller │ │ ├── ControllerClientHandler.java │ │ └── ControllerClient.java │ └── test │ └── java │ └── org │ └── sab │ └── controller │ └── ControllerTest.java ├── libs ├── netty │ ├── .dockerignore │ ├── Dockerfile │ └── src │ │ └── main │ │ └── java │ │ └── org │ │ └── sab │ │ └── netty │ │ ├── middleware │ │ └── QueueHandler.java │ │ └── ServerInitializer.java ├── arango │ └── src │ │ └── main │ │ └── resources │ │ └── config.properties ├── postgres │ └── src │ │ ├── main │ │ └── resources │ │ │ ├── sql │ │ │ ├── DropUsersTable.sql │ │ │ ├── CreateUsersTable.sql │ │ │ └── DropUserProcedures.sql │ │ │ └── config.example.json │ │ └── test │ │ └── java │ │ └── org │ │ └── sab │ │ └── postgres │ │ └── PostgresConnectionTest.java ├── redis │ └── src │ │ └── main │ │ └── resources │ │ └── config.properties ├── utilities │ └── src │ │ ├── main │ │ ├── resources │ │ │ ├── CharlieFlying │ │ │ ├── CharlieHopping │ │ │ ├── SampleImage.png │ │ │ └── org │ │ │ │ └── sab │ │ │ │ └── classes │ │ │ │ └── Alice.class │ │ └── java │ │ │ └── org │ │ │ └── sab │ │ │ ├── functions │ │ │ ├── TriFunction.java │ │ │ └── Utilities.java │ │ │ ├── classes │ │ │ ├── Bob.java │ │ │ ├── ClassRegistry.java │ │ │ ├── Reader.java │ │ │ └── ByteClassLoader.java │ │ │ ├── databases │ │ │ ├── PoolDoesNotExistException.java │ │ │ └── PooledDatabaseClient.java │ │ │ ├── validation │ │ │ ├── exceptions │ │ │ │ └── EnvironmentVariableNotLoaded.java │ │ │ ├── Schema.java │ │ │ └── Attribute.java │ │ │ ├── auth │ │ │ └── Auth.java │ │ │ ├── futures │ │ │ └── FutureUtil.java │ │ │ ├── reflection │ │ │ └── ReflectionUtils.java │ │ │ ├── minio │ │ │ └── FileSimulation.java │ │ │ ├── environmentvariables │ │ │ └── EnvVariablesUtils.java │ │ │ ├── io │ │ │ └── IoUtils.java │ │ │ ├── strings │ │ │ └── StringManipulation.java │ │ │ ├── tests │ │ │ └── TestsUtils.java │ │ │ └── HttpServerUtilities │ │ │ └── HttpClient.java │ │ └── test │ │ └── java │ │ └── org │ │ └── sab │ │ ├── functions │ │ └── TriFunctionTest.java │ │ ├── environmentvariables │ │ └── EnvVariablesUtilsTest.java │ │ └── futures │ │ └── FutureUtilTest.java ├── service │ └── src │ │ ├── main │ │ ├── resources │ │ │ └── dbmap.properties │ │ └── java │ │ │ └── org │ │ │ └── sab │ │ │ └── service │ │ │ ├── validation │ │ │ ├── HTTPMethod.java │ │ │ └── RequestVerificationException.java │ │ │ ├── Command.java │ │ │ ├── Service.java │ │ │ ├── ServiceConstants.java │ │ │ ├── Responder.java │ │ │ ├── managers │ │ │ ├── ThreadPoolManager.java │ │ │ └── QueueManager.java │ │ │ ├── databases │ │ │ └── DBConfig.java │ │ │ └── controllerbackdoor │ │ │ └── BackdoorServerHandler.java │ │ └── test │ │ └── java │ │ └── org │ │ └── sab │ │ └── service │ │ └── ServiceTest.java ├── couchbase │ ├── Dockerfile │ ├── configure-server.sh │ └── src │ │ └── main │ │ └── java │ │ └── org │ │ └── sab │ │ └── couchbase │ │ └── config │ │ └── buckets.json ├── models │ └── src │ │ └── main │ │ └── java │ │ └── org │ │ └── sab │ │ └── models │ │ ├── report │ │ ├── TypeOfReport.java │ │ └── SubThreadReportAttributes.java │ │ ├── EdgeCollectionsAttributes.java │ │ ├── AuthenticationAttributes.java │ │ ├── NotificationAttributes.java │ │ ├── RequestAttributes.java │ │ ├── CouchbaseBuckets.java │ │ ├── ThreadAttributes.java │ │ ├── SubThreadAttributes.java │ │ ├── CommentAttributes.java │ │ └── CollectionNames.java ├── rabbitmq │ └── src │ │ └── main │ │ └── java │ │ └── org │ │ └── sab │ │ ├── innerAppComm │ │ └── RequestType.java │ │ └── rabbitmq │ │ ├── RPCBase.java │ │ ├── SingleBasicChannel.java │ │ ├── RPCClient.java │ │ └── RPCServer.java └── example_util │ └── src │ ├── main │ └── java │ │ └── org │ │ └── sab │ │ └── strings │ │ └── ExampleUtil.java │ └── test │ └── java │ └── org │ └── sab │ └── strings │ └── ExampleUtilTest.java ├── notification-client ├── .vscode │ └── settings.json ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ ├── firebase-messaging-sw.js │ └── index.html ├── src │ ├── setupTests.js │ ├── App.test.js │ ├── index.css │ ├── reportWebVitals.js │ ├── index.js │ └── App.css ├── .gitignore └── package.json ├── cli ├── contexts │ ├── app-context.js │ └── chat-context.js ├── .editorconfig ├── .prettierrc ├── components │ ├── loading-spinner.js │ ├── welcome-header.js │ ├── messages-list.js │ ├── counter.js │ ├── message.js │ ├── create-group-chat.js │ ├── create-direct-chat.js │ ├── add-group-member.js │ ├── remove-group-member.js │ └── chat-view.js ├── test.js ├── readme.md ├── flows │ ├── welcome-flow.js │ └── chat-flow.js ├── cli.js ├── package.json └── app.js ├── docker-compose.yml ├── .idea ├── vcs.xml └── sqldialects.xml ├── README.md └── pom.xml /apps/search_app/.dockerignore: -------------------------------------------------------------------------------- 1 | target/ 2 | *.iml -------------------------------------------------------------------------------- /libs/netty/.dockerignore: -------------------------------------------------------------------------------- 1 | target/ 2 | *.iml 3 | -------------------------------------------------------------------------------- /apps/thread_app/.dockerignore: -------------------------------------------------------------------------------- 1 | target/ 2 | *.iml 3 | -------------------------------------------------------------------------------- /apps/user_app/.dockerignore: -------------------------------------------------------------------------------- 1 | target/ 2 | *.iml 3 | -------------------------------------------------------------------------------- /apps/notification_app/.dockerignore: -------------------------------------------------------------------------------- 1 | target/ 2 | *.iml 3 | -------------------------------------------------------------------------------- /apps/recommendation_app/.dockerignore: -------------------------------------------------------------------------------- 1 | target/ 2 | *.iml -------------------------------------------------------------------------------- /apps/subthread_app/.dockerignore: -------------------------------------------------------------------------------- 1 | target/ 2 | *.iml 3 | -------------------------------------------------------------------------------- /apps/chat_app/chat_server/.dockerignore: -------------------------------------------------------------------------------- 1 | target/ 2 | *.iml 3 | -------------------------------------------------------------------------------- /apps/chat_app/chat_storage/.dockerignore: -------------------------------------------------------------------------------- 1 | target/ 2 | *.iml 3 | -------------------------------------------------------------------------------- /apps/user_to_user_actions_app/.dockerignore: -------------------------------------------------------------------------------- 1 | target/ 2 | *.iml 3 | -------------------------------------------------------------------------------- /apps/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | COPY nginx.conf /etc/nginx/nginx.conf -------------------------------------------------------------------------------- /apps/notification_app/src/main/resources/config.properties: -------------------------------------------------------------------------------- 1 | threadsCount=11 2 | -------------------------------------------------------------------------------- /libs/arango/src/main/resources/config.properties: -------------------------------------------------------------------------------- 1 | NUMBER_OF_CONNECTIONS=10 2 | -------------------------------------------------------------------------------- /apps/thread_app/src/main/resources/cacheThreshold.properties: -------------------------------------------------------------------------------- 1 | THREAD_FOLLOWERS = 1000 -------------------------------------------------------------------------------- /apps/thread_app/src/main/resources/configurations.properties: -------------------------------------------------------------------------------- 1 | THREAD_RAM_QUOTA=100 -------------------------------------------------------------------------------- /notification-client/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true 3 | } -------------------------------------------------------------------------------- /apps/subthread_app/src/main/resources/configurations.properties: -------------------------------------------------------------------------------- 1 | COMMENT_RAM_QUOTA=100 2 | SUBTHREAD_RAM_QUOTA=100 3 | -------------------------------------------------------------------------------- /notification-client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /apps/example_app/src/main/resources/config.properties: -------------------------------------------------------------------------------- 1 | threadsCount=11 2 | requiredDatabases = org.sab.arango.Arango-10 3 | -------------------------------------------------------------------------------- /apps/search_app/src/main/resources/config.properties: -------------------------------------------------------------------------------- 1 | threadsCount=11 2 | requiredDatabases = org.sab.arango.Arango-10 3 | -------------------------------------------------------------------------------- /apps/thread_app/src/main/resources/config.properties: -------------------------------------------------------------------------------- 1 | threadsCount=11 2 | requiredDatabases = org.sab.arango.Arango-10 3 | -------------------------------------------------------------------------------- /apps/user_app/src/main/resources/config.properties: -------------------------------------------------------------------------------- 1 | threadsCount=11 2 | requiredDatabases = org.sab.arango.Arango-10 3 | -------------------------------------------------------------------------------- /libs/postgres/src/main/resources/sql/DropUsersTable.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE users CASCADE; 2 | DROP TABLE deleted_users CASCADE; 3 | -------------------------------------------------------------------------------- /apps/subthread_app/src/main/resources/config.properties: -------------------------------------------------------------------------------- 1 | threadsCount=11 2 | requiredDatabases = org.sab.arango.Arango-10 3 | -------------------------------------------------------------------------------- /notification-client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moe98/reddit/HEAD/notification-client/public/favicon.ico -------------------------------------------------------------------------------- /notification-client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moe98/reddit/HEAD/notification-client/public/logo192.png -------------------------------------------------------------------------------- /notification-client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moe98/reddit/HEAD/notification-client/public/logo512.png -------------------------------------------------------------------------------- /apps/recommendation_app/src/main/resources/config.properties: -------------------------------------------------------------------------------- 1 | threadsCount=11 2 | requiredDatabases = org.sab.arango.Arango-10 3 | -------------------------------------------------------------------------------- /libs/redis/src/main/resources/config.properties: -------------------------------------------------------------------------------- 1 | OPERATION_TIMEOUT_MINUTES=1 2 | DATABASE_NUMBER=0 3 | NUMBER_OF_CONNECTIONS=10 4 | -------------------------------------------------------------------------------- /apps/user_to_user_actions_app/src/main/resources/config.properties: -------------------------------------------------------------------------------- 1 | threadsCount=11 2 | requiredDatabases = org.sab.arango.Arango-10 3 | -------------------------------------------------------------------------------- /libs/utilities/src/main/resources/CharlieFlying: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moe98/reddit/HEAD/libs/utilities/src/main/resources/CharlieFlying -------------------------------------------------------------------------------- /apps/example_app/src/main/resources/ImprovedHello: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moe98/reddit/HEAD/apps/example_app/src/main/resources/ImprovedHello -------------------------------------------------------------------------------- /apps/example_app/src/main/resources/MorningWorld: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moe98/reddit/HEAD/apps/example_app/src/main/resources/MorningWorld -------------------------------------------------------------------------------- /libs/service/src/main/resources/dbmap.properties: -------------------------------------------------------------------------------- 1 | ARANGO = org.sab.arango.Arango 2 | CASSANDRA = org.sab.chat.storage.config.CassandraConnector 3 | -------------------------------------------------------------------------------- /libs/utilities/src/main/resources/CharlieHopping: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moe98/reddit/HEAD/libs/utilities/src/main/resources/CharlieHopping -------------------------------------------------------------------------------- /libs/utilities/src/main/resources/SampleImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moe98/reddit/HEAD/libs/utilities/src/main/resources/SampleImage.png -------------------------------------------------------------------------------- /apps/chat_app/chat_storage/src/main/resources/config.properties: -------------------------------------------------------------------------------- 1 | threadsCount=11 2 | requiredDatabases = org.sab.chat.storage.config.CassandraConnector-10 3 | -------------------------------------------------------------------------------- /apps/notification_app/.gitignore: -------------------------------------------------------------------------------- 1 | # Firebase SDK Private Key 2 | src/main/resources/service-account-file.json 3 | 4 | # Firebase Config 5 | firebase-config.json -------------------------------------------------------------------------------- /apps/subthread_app/src/main/resources/cacheThreshold.properties: -------------------------------------------------------------------------------- 1 | SUBTHREAD_LIKES=1000 2 | SUBTHREAD_DISLIKES=1000 3 | COMMENT_LIKES=1000 4 | COMMENT_DISLIKES=1000 -------------------------------------------------------------------------------- /cli/contexts/app-context.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const React = require('react') 3 | 4 | const AppContext = React.createContext() 5 | 6 | module.exports = AppContext 7 | -------------------------------------------------------------------------------- /libs/couchbase/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM couchbase 2 | COPY libs/couchbase/configure-server.sh /opt/couchbase/configure-server.sh 3 | CMD ["/opt/couchbase/configure-server.sh"] -------------------------------------------------------------------------------- /libs/models/src/main/java/org/sab/models/report/TypeOfReport.java: -------------------------------------------------------------------------------- 1 | package org.sab.models.report; 2 | 3 | public enum TypeOfReport { 4 | SUBTHREAD_REPORT; 5 | } 6 | -------------------------------------------------------------------------------- /apps/search_app/src/main/resources/commandmap.properties: -------------------------------------------------------------------------------- 1 | SEARCH_SUBTHREAD=org.sab.search.commands.SearchSubThread 2 | SEARCH_THREAD=org.sab.search.commands.SearchThread 3 | -------------------------------------------------------------------------------- /cli/contexts/chat-context.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const React = require('react') 3 | 4 | const ChatContext = React.createContext() 5 | 6 | module.exports = ChatContext 7 | -------------------------------------------------------------------------------- /libs/rabbitmq/src/main/java/org/sab/innerAppComm/RequestType.java: -------------------------------------------------------------------------------- 1 | package org.sab.innerAppComm; 2 | 3 | public enum RequestType { 4 | GET, PUT, POST, DELETE; 5 | } 6 | -------------------------------------------------------------------------------- /libs/utilities/src/main/resources/org/sab/classes/Alice.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moe98/reddit/HEAD/libs/utilities/src/main/resources/org/sab/classes/Alice.class -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.2" 2 | services: 3 | rabbitmq: 4 | image: rabbitmq:3-management-alpine 5 | ports: 6 | - 5672:5672 7 | - 15672:15672 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /libs/utilities/src/main/java/org/sab/functions/TriFunction.java: -------------------------------------------------------------------------------- 1 | package org.sab.functions; 2 | 3 | @FunctionalInterface 4 | public interface TriFunction { 5 | S apply(T a, U b); 6 | } 7 | -------------------------------------------------------------------------------- /apps/controller/src/main/resources/apps-ports.properties: -------------------------------------------------------------------------------- 1 | chat=4001 2 | example=4002 3 | notification=4003 4 | recommendation=4004 5 | search=4005 6 | subthread=4006 7 | thread=4007 8 | user=4008 9 | useraction=4009 -------------------------------------------------------------------------------- /apps/example_app/src/main/resources/commandmap.properties: -------------------------------------------------------------------------------- 1 | HELLO_WORLD = org.sab.demo.commands.HelloWorld 2 | GOOD_BYE_WORLD = org.sab.demo.commands.GoodByeWorld 3 | HELLO_ARANGO = org.sab.demo.commands.HelloArango -------------------------------------------------------------------------------- /.idea/sqldialects.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /libs/utilities/src/main/java/org/sab/classes/Bob.java: -------------------------------------------------------------------------------- 1 | package org.sab.classes; 2 | 3 | @SuppressWarnings("unused") 4 | public class Bob { 5 | 6 | public String talk() { 7 | return "Hi"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /cli/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /apps/recommendation_app/src/main/resources/config.development.json: -------------------------------------------------------------------------------- 1 | { 2 | "COUCHBASE_HOST": "127.0.0.1", 3 | "COUCHBASE_USERNAME": "", 4 | "COUCHBASE_PASSWORD": "", 5 | "ARANGO_USER": "", 6 | "ARANGO_PASSWORD": "", 7 | "ARANGO_DB": "Reddit" 8 | } -------------------------------------------------------------------------------- /cli/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "arrowParens": "always", 9 | "trailingComma": "none" 10 | } -------------------------------------------------------------------------------- /apps/controller/src/main/resources/apps-ips.properties: -------------------------------------------------------------------------------- 1 | chat_1=127.0.0.1 2 | example_1=127.0.0.1 3 | notification_1=127.0.0.1 4 | recommendation_1=127.0.0.1 5 | search_1=127.0.0.1 6 | subthread_1=127.0.0.1 7 | thread_1=127.0.0.1 8 | user_1=127.0.0.1 9 | useraction_1=127.0.0.1 -------------------------------------------------------------------------------- /apps/notification_app/src/main/resources/commandmap.properties: -------------------------------------------------------------------------------- 1 | SEND_NOTIFICATION=org.sab.notification.commands.SendNotification 2 | REGISTER_DEVICE_TOKEN=org.sab.notification.commands.RegisterDeviceToken 3 | GET_NOTIFICATIONS=org.sab.notification.commands.GetNotifications 4 | -------------------------------------------------------------------------------- /libs/utilities/src/main/java/org/sab/databases/PoolDoesNotExistException.java: -------------------------------------------------------------------------------- 1 | package org.sab.databases; 2 | 3 | public class PoolDoesNotExistException extends Exception{ 4 | 5 | public PoolDoesNotExistException(String message) { 6 | super(message); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /notification-client/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /libs/service/src/main/java/org/sab/service/validation/HTTPMethod.java: -------------------------------------------------------------------------------- 1 | package org.sab.service.validation; 2 | 3 | public enum HTTPMethod { 4 | GET, PUT, POST, DELETE; 5 | 6 | public boolean equals(String methodType) { 7 | return toString().equals(methodType); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /notification-client/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /libs/postgres/src/main/resources/config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "POSTGRES_HOST":"", 3 | "POSTGRES_PORT":"", 4 | "POSTGRES_DB":"", 5 | "POSTGRES_USER":"", 6 | "POSTGRES_PASSWORD":"", 7 | 8 | "Cloudinary Keys":"", 9 | "cloud_name":"", 10 | "api_key":"", 11 | "api_secret":"" 12 | } 13 | -------------------------------------------------------------------------------- /libs/service/src/main/java/org/sab/service/validation/RequestVerificationException.java: -------------------------------------------------------------------------------- 1 | package org.sab.service.validation; 2 | 3 | public class RequestVerificationException extends Exception { 4 | 5 | public RequestVerificationException(String message) { 6 | super(message); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /apps/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | http { 2 | upstream servers { 3 | least_conn; 4 | server 127.0.0.1:8081; 5 | server 127.0.0.1:8082; 6 | } 7 | 8 | server { 9 | listen 8080; 10 | location / { 11 | proxy_pass http://servers/; 12 | } 13 | } 14 | } 15 | 16 | events { } 17 | 18 | -------------------------------------------------------------------------------- /apps/notification_app/src/main/java/org/sab/notification/NotificationSendingFailedException.java: -------------------------------------------------------------------------------- 1 | package org.sab.notification; 2 | 3 | public class NotificationSendingFailedException extends Exception { 4 | public NotificationSendingFailedException(String message, Throwable cause) { 5 | super(message, cause); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/chat_app/chat_server/src/test/java/org/sab/chat/server/AppTest.java: -------------------------------------------------------------------------------- 1 | package org.sab.chat.server; 2 | 3 | import static org.junit.Assert.assertTrue; 4 | 5 | import org.junit.Test; 6 | 7 | public class AppTest { 8 | 9 | @Test 10 | public void shouldAnswerWithTrue() { 11 | assertTrue( true ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/chat_app/chat_storage/src/test/java/org/sab/chat/storage/AppTest.java: -------------------------------------------------------------------------------- 1 | package org.sab.chat.storage; 2 | 3 | import static org.junit.Assert.assertTrue; 4 | 5 | import org.junit.Test; 6 | 7 | public class AppTest { 8 | @Test 9 | public void shouldAnswerWithTrue() { 10 | assertTrue( true ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/controller/src/test/java/org/sab/controller/ControllerTest.java: -------------------------------------------------------------------------------- 1 | package org.sab.controller; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertTrue; 6 | 7 | 8 | public class ControllerTest { 9 | 10 | @Test 11 | public void shouldAnswerWithTrue() { 12 | assertTrue(true); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /apps/notification_app/src/main/java/org/sab/notification/GoogleCredentialsLoadingFailedException.java: -------------------------------------------------------------------------------- 1 | package org.sab.notification; 2 | 3 | public class GoogleCredentialsLoadingFailedException extends Exception { 4 | 5 | public GoogleCredentialsLoadingFailedException(String message, Throwable cause) { 6 | super(message, cause); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /apps/user_to_user_actions_app/src/main/resources/commandmap.properties: -------------------------------------------------------------------------------- 1 | BLOCK_USER=org.sab.useractions.commands.BlockUser 2 | FOLLOW_USER=org.sab.useractions.commands.FollowUser 3 | GET_BLOCKED_USERS=org.sab.useractions.commands.GetBlockedUsers 4 | GET_FOLLOWED_USERS=org.sab.useractions.commands.GetFollowedUsers 5 | GET_MY_FOLLOWERS=org.sab.useractions.commands.GetMyFollowers -------------------------------------------------------------------------------- /libs/example_util/src/main/java/org/sab/strings/ExampleUtil.java: -------------------------------------------------------------------------------- 1 | package org.sab.strings; 2 | 3 | import java.util.List; 4 | import java.util.stream.Collectors; 5 | 6 | public class ExampleUtil { 7 | 8 | public static List toCharList(String s) { 9 | return s.chars().mapToObj(c -> (char) c).collect(Collectors.toList()); 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /apps/example_app/src/main/java/org/sab/demo/commands/GoodByeWorld.java: -------------------------------------------------------------------------------- 1 | package org.sab.demo.commands; 2 | 3 | import org.json.JSONObject; 4 | import org.sab.service.Command; 5 | 6 | public class GoodByeWorld extends Command { 7 | 8 | @Override 9 | public String execute(JSONObject request) { 10 | return "{\"msg\":\"GoodBye World\"}"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /libs/utilities/src/main/java/org/sab/validation/exceptions/EnvironmentVariableNotLoaded.java: -------------------------------------------------------------------------------- 1 | package org.sab.validation.exceptions; 2 | 3 | public class EnvironmentVariableNotLoaded extends Exception { 4 | public EnvironmentVariableNotLoaded(String envVariableName) { 5 | super(String.format("The %s environment variable was not loaded", envVariableName)); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/models/src/main/java/org/sab/models/EdgeCollectionsAttributes.java: -------------------------------------------------------------------------------- 1 | package org.sab.models; 2 | 3 | public enum EdgeCollectionsAttributes { 4 | USER_FOLLOW_THREAD_DATE("Date"); 5 | 6 | private final String db; 7 | 8 | EdgeCollectionsAttributes(String db) { 9 | this.db = db; 10 | } 11 | 12 | public String getDb() { 13 | return db; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /cli/components/loading-spinner.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const { Text } = require('ink') 3 | const { default: Spinner } = require('ink-spinner') 4 | 5 | const LoadingSpinner = ({ loadingText = 'Loading' }) => ( 6 | 7 | 8 | 9 | 10 | {' ' + loadingText} 11 | 12 | ) 13 | 14 | module.exports = LoadingSpinner 15 | -------------------------------------------------------------------------------- /cli/components/welcome-header.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const Gradient = require('ink-gradient') 3 | const BigText = require('ink-big-text') 4 | 5 | const WelcomeHeader = ({ rainbow, text }) => { 6 | return rainbow ? ( 7 | 8 | 9 | 10 | ) : ( 11 | 12 | ) 13 | } 14 | 15 | module.exports = WelcomeHeader 16 | -------------------------------------------------------------------------------- /apps/chat_app/chat_storage/src/main/java/org/sab/chat/storage/ChatStorageApp.java: -------------------------------------------------------------------------------- 1 | package org.sab.chat.storage; 2 | 3 | import org.sab.service.Service; 4 | 5 | public class ChatStorageApp extends Service { 6 | 7 | @Override 8 | public String getAppUriName() { 9 | return "CHAT"; 10 | } 11 | 12 | public static void main(String[] args){ 13 | new ChatStorageApp().start(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /libs/service/src/test/java/org/sab/service/ServiceTest.java: -------------------------------------------------------------------------------- 1 | package org.sab.service; 2 | 3 | import static org.junit.Assert.assertTrue; 4 | 5 | import org.junit.Test; 6 | 7 | /** 8 | * Unit test for simple App. 9 | */ 10 | public class ServiceTest { 11 | /** 12 | * Rigorous Test :-) 13 | */ 14 | @Test 15 | public void shouldAnswerWithTrue() { 16 | assertTrue(true); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/thread_app/src/test/java/org/sab/thread/ThreadAppTest.java: -------------------------------------------------------------------------------- 1 | package org.sab.thread; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertTrue; 6 | 7 | /** 8 | * Unit test for simple App. 9 | */ 10 | public class ThreadAppTest { 11 | /** 12 | * Rigorous Test :-) 13 | */ 14 | @Test 15 | public void shouldAnswerWithTrue() { 16 | assertTrue(true); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/example_app/src/main/java/org/sab/demo/ExampleApp.java: -------------------------------------------------------------------------------- 1 | package org.sab.demo; 2 | 3 | import org.sab.service.Service; 4 | 5 | public class ExampleApp extends Service { 6 | 7 | public static void main(String[] args) { 8 | ExampleApp a = new ExampleApp(); 9 | a.start(); 10 | } 11 | 12 | @Override 13 | public String getAppUriName() { 14 | return "EXAMPLE"; 15 | } 16 | 17 | 18 | 19 | } 20 | -------------------------------------------------------------------------------- /apps/subthread_app/src/test/java/org/sab/subthread/SubThreadAppTest.java: -------------------------------------------------------------------------------- 1 | package org.sab.subthread; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertTrue; 6 | 7 | /** 8 | * Unit test for simple App. 9 | */ 10 | public class SubThreadAppTest { 11 | /** 12 | * Rigorous Test :-) 13 | */ 14 | @Test 15 | public void shouldAnswerWithTrue() { 16 | assertTrue(true); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /notification-client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /notification-client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /notification-client/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /libs/postgres/src/main/resources/sql/CreateUsersTable.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS users 2 | ( 3 | username character varying(255) PRIMARY KEY, 4 | email character varying(500) UNIQUE, 5 | password character varying(255), 6 | birthdate date, 7 | photo_url text, 8 | user_id character varying(50) UNIQUE 9 | ); 10 | 11 | 12 | CREATE TABLE IF NOT EXISTS deleted_users 13 | ( 14 | username character varying(255) PRIMARY KEY 15 | ); 16 | -------------------------------------------------------------------------------- /cli/components/messages-list.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const { Box } = require('ink') 3 | 4 | const importJsx = require('import-jsx') 5 | 6 | const Message = importJsx('./message') 7 | 8 | const MessagesList = ({ messages }) => { 9 | return ( 10 | 11 | {messages.map((message) => ( 12 | 13 | ))} 14 | 15 | ) 16 | } 17 | 18 | module.exports = MessagesList 19 | -------------------------------------------------------------------------------- /libs/models/src/main/java/org/sab/models/AuthenticationAttributes.java: -------------------------------------------------------------------------------- 1 | package org.sab.models; 2 | 3 | public enum AuthenticationAttributes { 4 | AUTHENTICATION_PARAMS("authenticationParams"), 5 | IS_AUTHENTICATED("isAuthenticated"); 6 | ; 7 | private final String value; 8 | 9 | AuthenticationAttributes(String value) { 10 | this.value = value; 11 | } 12 | 13 | public String getValue() { 14 | return value; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /apps/user_app/src/main/resources/commandmap.properties: -------------------------------------------------------------------------------- 1 | SIGN_UP=org.sab.user.commands.SignUp 2 | UPDATE_PROFILE_PHOTO=org.sab.user.commands.UpdateProfilePhoto 3 | DELETE_PROFILE_PHOTO=org.sab.user.commands.DeleteProfilePhoto 4 | DELETE_ACCOUNT=org.sab.user.commands.DeleteAccount 5 | UPDATE_PASSWORD=org.sab.user.commands.UpdatePassword 6 | VIEW_MY_PROFILE=org.sab.user.commands.ViewMyProfile 7 | VIEW_ANOTHER_PROFILE=org.sab.user.commands.ViewAnotherProfile 8 | LOGIN=org.sab.user.commands.Login -------------------------------------------------------------------------------- /libs/models/src/main/java/org/sab/models/NotificationAttributes.java: -------------------------------------------------------------------------------- 1 | package org.sab.models; 2 | 3 | public enum NotificationAttributes { 4 | 5 | USERS_LIST("usersList"), 6 | TITLE("title"), 7 | NOTIFICATION_BODY("notificationBody"); 8 | 9 | 10 | private final String value; 11 | 12 | NotificationAttributes(String value) { 13 | this.value = value; 14 | } 15 | 16 | public String getValue() { 17 | return value; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /libs/example_util/src/test/java/org/sab/strings/ExampleUtilTest.java: -------------------------------------------------------------------------------- 1 | package org.sab.strings; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.List; 6 | 7 | import static org.junit.Assert.assertEquals; 8 | 9 | public class ExampleUtilTest { 10 | 11 | @Test 12 | public void shouldDivideHelloIntoItsLetters() { 13 | final List chars = ExampleUtil.toCharList("Hello"); 14 | assertEquals(chars, List.of('H', 'e', 'l', 'l', 'o')); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cli/test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import chalk from 'chalk' 3 | import test from 'ava' 4 | import { render } from 'ink-testing-library' 5 | import App from './ui' 6 | 7 | test('greet unknown user', (t) => { 8 | const { lastFrame } = render() 9 | 10 | t.is(lastFrame(), chalk`Hello, {green Stranger}`) 11 | }) 12 | 13 | test('greet user with a name', (t) => { 14 | const { lastFrame } = render() 15 | 16 | t.is(lastFrame(), chalk`Hello, {green Jane}`) 17 | }) 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reddit 2 | 3 | A scalable Reddit replica that is able to handle ~2,300 concurrent users, each performing 10 requests per second for 10 minutes straight, with a mean response time of ~5 seconds. 4 | 5 | ## Technologies & Tools 6 | 7 | ### Server 8 | 9 | - Netty 10 | - RabbitMQ 11 | 12 | ### Load Balancing 13 | 14 | - Nginx 15 | - Minikube 16 | 17 | ### Databases 18 | 19 | - PostgreSQL 20 | - ArangoDB 21 | - Cassandra 22 | - Firebase 23 | 24 | ### Cache 25 | 26 | - Couchbase 27 | - Redis **Lettuce** 28 | -------------------------------------------------------------------------------- /apps/notification_app/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | rabbitmq: 4 | image: "rabbitmq:management" 5 | ports: 6 | - "15672:15672" 7 | - "5762:5762" 8 | notificiation_app: 9 | build: 10 | context: "../../" 11 | dockerfile: "apps/notification_app/Dockerfile" 12 | restart: on-failure 13 | ports: 14 | - "4003:4003" 15 | env_file: 16 | - "../../.env" 17 | depends_on: 18 | - "rabbitmq" 19 | networks: 20 | default: 21 | name: notification_nw 22 | -------------------------------------------------------------------------------- /cli/components/counter.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const { Text } = require('ink') 3 | 4 | const { useState, useEffect } = React 5 | 6 | const Counter = () => { 7 | const [counter, setCounter] = useState(0) 8 | 9 | useEffect(() => { 10 | const timer = setInterval(() => { 11 | setCounter((previousCounter) => previousCounter + 1) 12 | }, 100) 13 | 14 | return () => { 15 | clearInterval(timer) 16 | } 17 | }, []) 18 | 19 | return x{counter} 20 | } 21 | 22 | module.exports = Counter 23 | -------------------------------------------------------------------------------- /libs/models/src/main/java/org/sab/models/RequestAttributes.java: -------------------------------------------------------------------------------- 1 | package org.sab.models; 2 | 3 | public enum RequestAttributes { 4 | BODY("body"), 5 | METHOD_TYPE("methodType"), 6 | AUTHENTICATION_PARAMS("authenticationParams"), 7 | FUNCTION_NAME("functionName"), 8 | URI_PARAMS("uriParams"); 9 | 10 | private final String value; 11 | 12 | RequestAttributes(String value) { 13 | this.value = value; 14 | } 15 | 16 | public String getValue() { 17 | return value; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /apps/example_app/src/main/java/org/sab/demo/commands/HelloWorld.java: -------------------------------------------------------------------------------- 1 | package org.sab.demo.commands; 2 | 3 | import org.json.JSONObject; 4 | import org.sab.service.Command; 5 | 6 | 7 | 8 | public class HelloWorld extends Command { 9 | 10 | @Override 11 | public String execute(JSONObject request) { 12 | // Return SUCCESS 13 | return "{\"msg\":\"Hello World\", \"statusCode\": 200}"; 14 | // Return ERROR Ex: 400 for bad request 15 | // return "{\"msg\":\"Hello World\", \"statusCode\": 400}"; 16 | } 17 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.sab 6 | reddit 7 | 1.0-SNAPSHOT 8 | 9 | libs 10 | apps 11 | 12 | pom 13 | 14 | 15 | -------------------------------------------------------------------------------- /apps/thread_app/src/main/resources/commandmap.properties: -------------------------------------------------------------------------------- 1 | ASSIGN_THREAD_MODERATOR=org.sab.thread.commands.AssignThreadModerator 2 | BOOKMARK_THREAD=org.sab.thread.commands.BookmarkThread 3 | CREATE_THREAD=org.sab.thread.commands.CreateThread 4 | DELETE_THREAD=org.sab.thread.commands.DeleteThread 5 | FOLLOW_THREAD=org.sab.thread.commands.FollowThread 6 | GET_FOLLOWED_THREADS=org.sab.thread.commands.GetFollowedThreads 7 | GET_THREAD=org.sab.thread.commands.GetThread 8 | MODERATOR_BANS_USER=org.sab.thread.commands.ModeratorBansUser 9 | UPDATE_THREAD=org.sab.thread.commands.UpdateThread 10 | -------------------------------------------------------------------------------- /apps/search_app/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Build stage 4 | FROM maven:3.8.1-openjdk-15-slim AS build 5 | ENV APP_NAME=search_app 6 | 7 | COPY pom.xml /reddit/ 8 | WORKDIR /reddit 9 | 10 | COPY libs libs 11 | COPY apps apps 12 | 13 | RUN mvn clean package -pl :${APP_NAME} -am -DskipTests 14 | 15 | # Package stage 16 | FROM openjdk:15-alpine 17 | ENV APP_NAME=search_app 18 | 19 | COPY --from=build /reddit/apps/${APP_NAME}/target/${APP_NAME}-1.0-SNAPSHOT.jar /usr/local/lib/${APP_NAME}.jar 20 | 21 | ENTRYPOINT ["java", "-jar", "/usr/local/lib/search_app.jar"] 22 | -------------------------------------------------------------------------------- /apps/thread_app/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Build stage 4 | FROM maven:3.8.1-openjdk-15-slim AS build 5 | ENV APP_NAME=thread_app 6 | 7 | COPY pom.xml /reddit/ 8 | WORKDIR /reddit 9 | 10 | COPY libs libs 11 | COPY apps apps 12 | 13 | RUN mvn clean package -pl :${APP_NAME} -am -DskipTests 14 | 15 | # Package stage 16 | FROM openjdk:15 17 | ENV APP_NAME=thread_app 18 | 19 | COPY --from=build /reddit/apps/${APP_NAME}/target/${APP_NAME}-1.0-SNAPSHOT.jar /usr/local/lib/${APP_NAME}.jar 20 | 21 | 22 | ENTRYPOINT ["java", "-jar", "/usr/local/lib/thread_app.jar"] 23 | -------------------------------------------------------------------------------- /apps/user_app/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Build stage 4 | FROM maven:3.8.1-openjdk-15-slim AS build 5 | ENV APP_NAME=user_app 6 | 7 | COPY pom.xml /reddit/ 8 | WORKDIR /reddit 9 | 10 | COPY libs libs 11 | COPY apps apps 12 | 13 | RUN mvn clean package -pl :${APP_NAME} -am -DskipTests 14 | 15 | # Package stage 16 | FROM openjdk:15-alpine 17 | ENV APP_NAME=user_app 18 | 19 | COPY --from=build /reddit/apps/${APP_NAME}/target/${APP_NAME}-1.0-SNAPSHOT.jar /usr/local/lib/${APP_NAME}.jar 20 | 21 | 22 | ENTRYPOINT ["java", "-jar", "/usr/local/lib/user_app.jar"] 23 | -------------------------------------------------------------------------------- /libs/models/src/main/java/org/sab/models/CouchbaseBuckets.java: -------------------------------------------------------------------------------- 1 | package org.sab.models; 2 | 3 | public enum CouchbaseBuckets { 4 | LISTINGS("Listings"), 5 | RECOMMENDED_SUB_THREADS("RecommendedSubThreads"), 6 | RECOMMENDED_THREADS("RecommendedThreads"), 7 | RECOMMENDED_USERS("RecommendedUsers"), 8 | COMMENTS("Comments"); 9 | 10 | 11 | private final String bucketName; 12 | 13 | CouchbaseBuckets(String bucketName) { 14 | this.bucketName = bucketName; 15 | } 16 | 17 | public String get() { 18 | return this.bucketName; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /libs/utilities/src/main/java/org/sab/auth/Auth.java: -------------------------------------------------------------------------------- 1 | package org.sab.auth; 2 | 3 | import org.mindrot.jbcrypt.BCrypt; 4 | 5 | public class Auth { 6 | private Auth() { 7 | } 8 | 9 | public static String hash(String password) { 10 | return BCrypt.hashpw(password, BCrypt.gensalt(12)); 11 | } 12 | 13 | public static boolean verifyHash(String password, String hash) { 14 | try { 15 | return BCrypt.checkpw(password, hash); 16 | } catch (IllegalArgumentException e) { 17 | return false; 18 | } 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/subthread_app/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Build stage 4 | FROM maven:3.8.1-openjdk-15-slim AS build 5 | ENV APP_NAME=subthread_app 6 | 7 | COPY pom.xml /reddit/ 8 | WORKDIR /reddit 9 | 10 | COPY libs libs 11 | COPY apps apps 12 | 13 | RUN mvn clean package -pl :${APP_NAME} -am -DskipTests 14 | 15 | # Package stage 16 | FROM openjdk:15 17 | ENV APP_NAME=subthread_app 18 | 19 | COPY --from=build /reddit/apps/${APP_NAME}/target/${APP_NAME}-1.0-SNAPSHOT.jar /usr/local/lib/${APP_NAME}.jar 20 | 21 | 22 | ENTRYPOINT ["java", "-jar", "/usr/local/lib/subthread_app.jar"] 23 | -------------------------------------------------------------------------------- /notification-client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /libs/netty/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Build stage 4 | FROM maven:3.8.1-openjdk-15-slim AS build 5 | ENV APP_NAME=netty 6 | 7 | COPY pom.xml /reddit/ 8 | WORKDIR /reddit 9 | 10 | COPY libs libs 11 | COPY apps apps 12 | 13 | RUN mvn clean package -pl :${APP_NAME} -am -DskipTests 14 | 15 | # Package stage 16 | FROM openjdk:15-alpine 17 | ENV APP_NAME=netty 18 | 19 | COPY --from=build /reddit/libs/${APP_NAME}/target/${APP_NAME}-1.0-SNAPSHOT.jar /usr/local/lib/${APP_NAME}.jar 20 | EXPOSE 8080 21 | EXPOSE 8443 22 | 23 | ENTRYPOINT ["java", "-jar", "/usr/local/lib/netty.jar"] 24 | -------------------------------------------------------------------------------- /libs/utilities/src/test/java/org/sab/functions/TriFunctionTest.java: -------------------------------------------------------------------------------- 1 | package org.sab.functions; 2 | import org.junit.Test; 3 | 4 | import static org.junit.Assert.assertEquals; 5 | 6 | public class TriFunctionTest { 7 | 8 | @Test 9 | public void shouldAddTwoIntegers() { 10 | final TriFunction add = (x, y) -> x + y; 11 | 12 | final Integer x = 3; 13 | final Integer y = 7; 14 | final Integer expected = x + y; 15 | final Integer actual = add.apply(x, y); 16 | 17 | assertEquals(expected, actual); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /apps/chat_app/chat_storage/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Build stage 4 | FROM maven:3.8.1-openjdk-15-slim AS build 5 | ENV APP_NAME=chat_storage 6 | 7 | COPY pom.xml /reddit/ 8 | WORKDIR /reddit 9 | 10 | COPY libs libs 11 | COPY apps apps 12 | 13 | RUN mvn clean package -pl :${APP_NAME} -am -DskipTests 14 | 15 | # Package stage 16 | FROM openjdk:15-alpine 17 | ENV APP_NAME=chat_storage 18 | 19 | COPY --from=build /reddit/apps/chat_app/${APP_NAME}/target/${APP_NAME}-1.0-SNAPSHOT.jar /usr/local/lib/${APP_NAME}.jar 20 | 21 | ENTRYPOINT ["java", "-jar", "/usr/local/lib/chat_storage.jar"] 22 | -------------------------------------------------------------------------------- /apps/recommendation_app/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Build stage 4 | FROM maven:3.8.1-openjdk-15-slim AS build 5 | ENV APP_NAME=recommendation_app 6 | 7 | COPY pom.xml /reddit/ 8 | WORKDIR /reddit 9 | 10 | COPY libs libs 11 | COPY apps apps 12 | 13 | RUN mvn clean package -pl :${APP_NAME} -am -DskipTests 14 | 15 | # Package stage 16 | FROM openjdk:15 17 | ENV APP_NAME=recommendation_app 18 | 19 | COPY --from=build /reddit/apps/${APP_NAME}/target/${APP_NAME}-1.0-SNAPSHOT.jar /usr/local/lib/${APP_NAME}.jar 20 | 21 | ENTRYPOINT ["java", "-jar", "/usr/local/lib/recommendation_app.jar"] 22 | -------------------------------------------------------------------------------- /apps/notification_app/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Build stage 4 | FROM maven:3.8.1-openjdk-15-slim AS build 5 | ENV APP_NAME=notification_app 6 | 7 | COPY pom.xml /reddit/ 8 | WORKDIR /reddit 9 | 10 | COPY libs libs 11 | COPY apps apps 12 | 13 | RUN mvn clean package -pl :${APP_NAME} -am -DskipTests 14 | 15 | # Package stage 16 | FROM openjdk:15-alpine 17 | ENV APP_NAME=notification_app 18 | 19 | COPY --from=build /reddit/apps/${APP_NAME}/target/${APP_NAME}-1.0-SNAPSHOT.jar /usr/local/lib/${APP_NAME}.jar 20 | 21 | 22 | ENTRYPOINT ["java", "-jar", "/usr/local/lib/notification_app.jar"] 23 | -------------------------------------------------------------------------------- /libs/utilities/src/main/java/org/sab/validation/Schema.java: -------------------------------------------------------------------------------- 1 | package org.sab.validation; 2 | 3 | import java.util.List; 4 | 5 | public class Schema { 6 | List attributeList; 7 | 8 | public Schema(List attributeList) { 9 | this.attributeList = attributeList; 10 | } 11 | 12 | public static Schema emptySchema() { 13 | return new Schema(List.of()); 14 | } 15 | 16 | public List getAttributeList() { 17 | return attributeList; 18 | } 19 | 20 | public boolean isEmpty() { 21 | return attributeList.isEmpty(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/chat_app/chat_server/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Build stage 4 | FROM maven:3.8.1-openjdk-15-slim AS build 5 | ENV APP_NAME=chat_server 6 | 7 | COPY pom.xml /reddit/ 8 | WORKDIR /reddit 9 | 10 | COPY libs libs 11 | COPY apps apps 12 | 13 | RUN mvn clean package -pl :${APP_NAME} -am -DskipTests 14 | 15 | # Package stage 16 | FROM openjdk:15-alpine 17 | ENV APP_NAME=chat_server 18 | 19 | COPY --from=build /reddit/apps/chat_app/${APP_NAME}/target/${APP_NAME}-1.0-SNAPSHOT.jar /usr/local/lib/${APP_NAME}.jar 20 | EXPOSE 5000 21 | 22 | ENTRYPOINT ["java", "-jar", "/usr/local/lib/chat_server.jar"] 23 | -------------------------------------------------------------------------------- /apps/chat_app/chat_storage/src/main/java/org/sab/chat/storage/exceptions/InvalidInputException.java: -------------------------------------------------------------------------------- 1 | package org.sab.chat.storage.exceptions; 2 | 3 | public class InvalidInputException extends Exception { 4 | static private final String DEFAULT_MESSAGE = "Inputs provided to are invalid."; 5 | 6 | public InvalidInputException() { 7 | super(DEFAULT_MESSAGE); 8 | } 9 | 10 | public InvalidInputException(Exception e) { 11 | super(DEFAULT_MESSAGE); 12 | e.printStackTrace(); 13 | } 14 | 15 | public InvalidInputException(String message) { 16 | super(message); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /notification-client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /apps/user_to_user_actions_app/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Build stage 4 | FROM maven:3.8.1-openjdk-15-slim AS build 5 | ENV APP_NAME=user_to_user_actions_app 6 | 7 | COPY pom.xml /reddit/ 8 | WORKDIR /reddit 9 | 10 | COPY libs libs 11 | COPY apps apps 12 | 13 | RUN mvn clean package -pl :${APP_NAME} -am -DskipTests 14 | 15 | # Package stage 16 | FROM openjdk:15-alpine 17 | ENV APP_NAME=user_to_user_actions_app 18 | 19 | COPY --from=build /reddit/apps/${APP_NAME}/target/${APP_NAME}-1.0-SNAPSHOT.jar /usr/local/lib/${APP_NAME}.jar 20 | 21 | 22 | ENTRYPOINT ["java", "-jar", "/usr/local/lib/user_to_user_actions_app.jar"] 23 | -------------------------------------------------------------------------------- /libs/utilities/src/main/java/org/sab/functions/Utilities.java: -------------------------------------------------------------------------------- 1 | package org.sab.functions; 2 | 3 | public class Utilities { 4 | private Utilities() { 5 | } 6 | 7 | public static boolean isDevelopmentMode() { 8 | String mode = System.getenv("ENV_TYPE"); 9 | return mode != null && mode.equals("Development"); 10 | } 11 | public static boolean inContainerizationMode() { 12 | String mode = System.getenv("ENV_TYPE"); 13 | return mode != null && mode.equals("Staging"); 14 | } 15 | 16 | public static String formatUUID(String UUID) { 17 | return UUID.replaceAll("[-]", ""); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /libs/utilities/src/main/java/org/sab/futures/FutureUtil.java: -------------------------------------------------------------------------------- 1 | package org.sab.futures; 2 | 3 | import java.util.concurrent.ExecutionException; 4 | import java.util.concurrent.Future; 5 | import java.util.function.Function; 6 | 7 | public class FutureUtil { 8 | private FutureUtil() { 9 | } 10 | 11 | public static T await(Future future, Function onFailure) { 12 | T result = null; 13 | try { 14 | result = future.get(); 15 | } catch (InterruptedException | ExecutionException e) { 16 | return onFailure.apply(e); 17 | } 18 | return result; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /libs/couchbase/configure-server.sh: -------------------------------------------------------------------------------- 1 | set -m 2 | 3 | /entrypoint.sh couchbase-server & 4 | 5 | 6 | sleep 15 7 | 8 | 9 | # Setup initial cluster/ Initialize Node 10 | couchbase-cli cluster-init -c 127.0.0.1 --cluster-name $COUCHBASE_CLUSTER_NAME --cluster-username $COUCHBASE_ADMINISTRATOR_USERNAME \ 11 | --cluster-password $COUCHBASE_ADMINISTRATOR_PASSWORD --services data --cluster-ramsize $COUCHBASE_RAM --index-storage-setting default \ 12 | 13 | # Setup Administrator username and password 14 | curl -v http://127.0.0.1:8091/settings/web -d port=8091 -d username=$COUCHBASE_ADMINISTRATOR_USERNAME -d password=$COUCHBASE_ADMINISTRATOR_PASSWORD 15 | 16 | fg 1 -------------------------------------------------------------------------------- /libs/utilities/src/main/java/org/sab/reflection/ReflectionUtils.java: -------------------------------------------------------------------------------- 1 | package org.sab.reflection; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | public class ReflectionUtils { 6 | public static Method getMethod(Class clas, String methodName) { 7 | try { 8 | Method[] methods = clas.getMethods(); 9 | for (Method method : methods) { 10 | if (method.getName().equalsIgnoreCase(methodName)) { 11 | return method; 12 | } 13 | } 14 | } catch (SecurityException e) { 15 | e.printStackTrace(); 16 | } 17 | return null; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/chat_app/chat_server/src/main/java/org/sab/chat/server/handlers/ResponseHandler.java: -------------------------------------------------------------------------------- 1 | package org.sab.chat.server.handlers; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import io.netty.channel.SimpleChannelInboundHandler; 5 | import org.json.simple.JSONObject; 6 | import org.sab.chat.server.ClientManager; 7 | 8 | public class ResponseHandler extends SimpleChannelInboundHandler { 9 | 10 | @Override 11 | protected void channelRead0(ChannelHandlerContext ctx, JSONObject queueResponse) { 12 | queueResponse.remove("statusCode"); 13 | ClientManager.routeResponse((JSONObject) queueResponse.clone(), ctx); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /cli/readme.md: -------------------------------------------------------------------------------- 1 | # reddit-cli 2 | 3 | ## Install 4 | Change directory to `reddit/cli`, then run the following commands: 5 | ```bash 6 | $ npm i 7 | $ npm install --global . 8 | ``` 9 | 10 | ## Using the CLI 11 | 12 | ``` 13 | $ reddit-cli --help 14 | 15 | Usage 16 | $ reddit-cli 17 | 18 | Commands 19 | $ reddit-cli welcome 20 | $ reddit-cli chat 21 | 22 | Flags 23 | --user, -u specify the user id. Special user ids are joe, ouda, abu and ronic. 24 | --rainbow, -r Add rainbow welcome title 25 | 26 | Examples 27 | $ reddit-cli chat -u ee55dcf8-ee7b-429a-939e-12c2f7b7ddee 28 | $ reddit-cli chat -u abu 29 | $ reddit-cli welcome -r 30 | ``` 31 | -------------------------------------------------------------------------------- /libs/service/src/main/java/org/sab/service/Command.java: -------------------------------------------------------------------------------- 1 | package org.sab.service; 2 | 3 | import org.json.JSONObject; 4 | 5 | /** 6 | * Abstract class Command that is extended by all command classes. 7 | */ 8 | 9 | public abstract class Command { 10 | private static double classVersion = 1.0; 11 | 12 | // The function that will be invoked by all classes implementing Command. 13 | public abstract String execute(JSONObject request); 14 | 15 | public static double getClassVersion() { 16 | return classVersion; 17 | } 18 | 19 | public static void setClassVersion(double classVersion) { 20 | Command.classVersion = classVersion; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /cli/flows/welcome-flow.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const React = require('react') 3 | const { Text, Static } = require('ink') 4 | const importJsx = require('import-jsx') 5 | 6 | const WelcomeHeader = importJsx('../components/welcome-header.js') 7 | const Counter = importJsx('../components/counter') 8 | 9 | const WelcomeFlow = ({ rainbow }) => ( 10 | 11 | 12 | {(item) => ( 13 | 14 | )} 15 | 16 | 17 | Hello, Stranger 18 | 19 | 20 | 21 | ) 22 | 23 | module.exports = WelcomeFlow 24 | -------------------------------------------------------------------------------- /apps/notification_app/src/main/java/org/sab/notification/NotificationApp.java: -------------------------------------------------------------------------------- 1 | package org.sab.notification; 2 | 3 | import org.sab.service.Service; 4 | 5 | public class NotificationApp extends Service { 6 | public static final String TOKEN = "token"; 7 | public static final String TOKENS_COLLECTION = "userTokens"; 8 | 9 | public static void main(String[] args) { 10 | new NotificationApp().start(); 11 | } 12 | 13 | public static String getNotificationsCollectionName(String user) { 14 | return "userNotifications/" + user + "/notifications"; 15 | } 16 | 17 | @Override 18 | public String getAppUriName() { 19 | return "notification"; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /apps/chat_app/chat_storage/src/main/resources/commandmap.properties: -------------------------------------------------------------------------------- 1 | ADD_GROUP_MEMBER = org.sab.chat.commands.AddGroupMember 2 | CREATE_DIRECT_CHAT = org.sab.chat.commands.CreateDirectChat 3 | CREATE_DIRECT_MESSAGE = org.sab.chat.commands.CreateDirectMessage 4 | CREATE_GROUP_CHAT = org.sab.chat.commands.CreateGroupChat 5 | CREATE_GROUP_MESSAGE = org.sab.chat.commands.CreateGroupMessage 6 | GET_CHATS = org.sab.chat.commands.GetChats 7 | GET_DIRECT_MESSAGES = org.sab.chat.commands.GetDirectMessages 8 | GET_GROUP_MESSAGES = org.sab.chat.commands.GetGroupMessages 9 | LEAVE_GROUP = org.sab.chat.commands.LeaveGroup 10 | REMOVE_GROUP_MEMBER = org.sab.chat.commands.RemoveGroupMember 11 | INIT_CONNECTION = org.sab.chat.commands.GetChats 12 | -------------------------------------------------------------------------------- /apps/user_to_user_actions_app/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | rabbitmq: 4 | image: "rabbitmq:management" 5 | ports: 6 | - "15672:15672" 7 | - "5672:5672" 8 | arangodb: 9 | image: "arangodb" 10 | environment: 11 | - ARANGO_ROOT_PASSWORD=root 12 | ports: 13 | - "8529:8529" 14 | user_to_user_actions_app: 15 | build: 16 | context: "../../" 17 | dockerfile: "apps/user_to_user_actions_app/Dockerfile" 18 | restart: on-failure 19 | ports: 20 | - "4009:4009" 21 | env_file: 22 | - "../../.env" 23 | 24 | depends_on: 25 | - "rabbitmq" 26 | - "arangodb" 27 | 28 | networks: 29 | default: 30 | name: user_actions_nw 31 | -------------------------------------------------------------------------------- /libs/service/src/main/java/org/sab/service/Service.java: -------------------------------------------------------------------------------- 1 | package org.sab.service; 2 | 3 | import org.sab.service.managers.ControlManager; 4 | 5 | /** 6 | * Abstract class service which will be extended by the main class of each mini-app. 7 | * Uses the Command Pattern. 8 | * Contains the threading and command invoking functionality. 9 | */ 10 | 11 | public abstract class Service { 12 | 13 | private final ControlManager controlManager = new ControlManager(getAppUriName()); 14 | 15 | public abstract String getAppUriName(); 16 | 17 | public void start() { 18 | controlManager.start(); 19 | } 20 | 21 | public ControlManager getControlManager() { 22 | return controlManager; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/example_app/src/test/java/org/sab/demo/CommandWithoutAuthTest.java: -------------------------------------------------------------------------------- 1 | package org.sab.demo; 2 | 3 | import org.json.JSONObject; 4 | import org.junit.Test; 5 | import org.sab.demo.commands.CommandWithoutAuth; 6 | 7 | import static org.junit.Assert.assertEquals; 8 | 9 | public class CommandWithoutAuthTest { 10 | 11 | @Test 12 | public void willSucceedWithoutAuth() { 13 | JSONObject request = new JSONObject().put("uriParams", new JSONObject()).put("methodType", "GET"); 14 | JSONObject response = new JSONObject(new CommandWithoutAuth().execute(request)); 15 | assertEquals(200, response.getInt("statusCode")); 16 | assertEquals("I don't need authentication!", response.getString("msg")); 17 | } 18 | 19 | 20 | } 21 | -------------------------------------------------------------------------------- /notification-client/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /apps/example_app/src/main/java/org/sab/demo/commands/CommandWithoutAuth.java: -------------------------------------------------------------------------------- 1 | package org.sab.demo.commands; 2 | 3 | import org.sab.service.Responder; 4 | import org.sab.service.validation.CommandWithVerification; 5 | import org.sab.service.validation.HTTPMethod; 6 | import org.sab.validation.Schema; 7 | 8 | public class CommandWithoutAuth extends CommandWithVerification { 9 | 10 | 11 | // isAuthNeeded is by default false 12 | 13 | @Override 14 | protected String execute() { 15 | return Responder.makeMsgResponse("I don't need authentication!"); 16 | } 17 | 18 | @Override 19 | protected Schema getSchema() { 20 | return Schema.emptySchema(); 21 | } 22 | 23 | @Override 24 | protected HTTPMethod getMethodType() { 25 | return HTTPMethod.GET; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apps/chat_app/chat_storage/src/main/java/org/sab/chat/storage/config/KeyspaceInitializer.java: -------------------------------------------------------------------------------- 1 | package org.sab.chat.storage.config; 2 | 3 | import com.datastax.driver.core.Session; 4 | 5 | public class KeyspaceInitializer { 6 | 7 | public static void initializeKeyspace(Session session, 8 | String keyspaceName, String replicationStrategy, int replicationFactor) { 9 | String query = String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH " + 10 | "replication = {'class':'%s', 'replication_factor': %d};", 11 | keyspaceName, replicationStrategy, replicationFactor 12 | ); 13 | session.execute(query); 14 | 15 | query = String.format("USE %s;", keyspaceName); 16 | session.execute(query); 17 | 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /libs/rabbitmq/src/main/java/org/sab/rabbitmq/RPCBase.java: -------------------------------------------------------------------------------- 1 | package org.sab.rabbitmq; 2 | 3 | import com.rabbitmq.client.Connection; 4 | import com.rabbitmq.client.ConnectionFactory; 5 | 6 | import java.io.IOException; 7 | import java.util.concurrent.TimeoutException; 8 | 9 | 10 | class RPCBase { 11 | 12 | private static final String LOCALHOST = "localhost"; 13 | 14 | private RPCBase() { 15 | } 16 | 17 | public static Connection initConnection() throws TimeoutException, IOException { 18 | ConnectionFactory factory = new ConnectionFactory(); 19 | 20 | String rabbitHost = System.getenv("RABBIT_HOST"); 21 | if(rabbitHost == null) 22 | rabbitHost = LOCALHOST; 23 | 24 | factory.setHost(rabbitHost); 25 | 26 | return factory.newConnection(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/user_to_user_actions_app/src/main/java/org/sab/useractions/UserToUserActionsApp.java: -------------------------------------------------------------------------------- 1 | package org.sab.useractions; 2 | 3 | import org.sab.service.Service; 4 | import org.sab.arango.Arango; 5 | 6 | public class UserToUserActionsApp extends Service { 7 | 8 | public static void main(String[] args) { 9 | try { 10 | new UserToUserActionsApp().start(); 11 | dbInit(); 12 | } catch (Exception e) { 13 | e.printStackTrace(); 14 | System.exit(-1); 15 | } 16 | } 17 | 18 | public static void dbInit() { 19 | Arango arango = Arango.getInstance(); 20 | arango.createDatabaseIfNotExists(System.getenv("ARANGO_DB")); 21 | } 22 | 23 | @Override 24 | public String getAppUriName() { 25 | return "useraction"; 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /libs/utilities/src/test/java/org/sab/environmentvariables/EnvVariablesUtilsTest.java: -------------------------------------------------------------------------------- 1 | package org.sab.environmentvariables; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | import static org.junit.Assert.fail; 7 | 8 | public class EnvVariablesUtilsTest { 9 | 10 | @Test 11 | public void getEnvOrThrowThrowsNullPointerIfEnvVariableDoesNotExist(){ 12 | String dummyEnvVariable = "ThisEnvVariableDoesNotExist"; 13 | try { 14 | EnvVariablesUtils.getEnvOrThrow(dummyEnvVariable); 15 | fail("This test should have thrown an exception, but it didn't !"); 16 | } 17 | catch (NullPointerException e){ 18 | assertEquals(String.format("The Environment Variable \"%s\" cannot be null", dummyEnvVariable),e.getMessage()); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /libs/utilities/src/main/java/org/sab/minio/FileSimulation.java: -------------------------------------------------------------------------------- 1 | package org.sab.minio; 2 | 3 | import org.json.JSONObject; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | 8 | public class FileSimulation { 9 | private static final String SAMPLE_IMAGE_NAME = "SampleImage.png"; 10 | private static final String SAMPLE_IMAGE_TYPE = "image/png"; 11 | 12 | public static JSONObject generateImageJson() throws IOException { 13 | InputStream is = FileSimulation.class.getClassLoader().getResourceAsStream(SAMPLE_IMAGE_NAME); 14 | if (is != null) { 15 | String data = org.apache.commons.codec.binary.Base64.encodeBase64String(is.readAllBytes()); 16 | return new JSONObject().put("data", data).put("type", SAMPLE_IMAGE_TYPE); 17 | } else 18 | return null; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/example_app/src/main/java/org/sab/demo/commands/CommandNeedingAuth.java: -------------------------------------------------------------------------------- 1 | package org.sab.demo.commands; 2 | 3 | import org.sab.service.Responder; 4 | import org.sab.service.validation.CommandWithVerification; 5 | import org.sab.service.validation.HTTPMethod; 6 | import org.sab.validation.Schema; 7 | 8 | public class CommandNeedingAuth extends CommandWithVerification { 9 | 10 | @Override 11 | protected boolean isAuthNeeded() { 12 | return true; 13 | } 14 | 15 | @Override 16 | protected String execute() { 17 | return Responder.makeMsgResponse("Authentication successful!"); 18 | } 19 | 20 | @Override 21 | protected Schema getSchema() { 22 | return Schema.emptySchema(); 23 | } 24 | 25 | @Override 26 | protected HTTPMethod getMethodType() { 27 | return HTTPMethod.GET; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/recommendation_app/src/main/resources/commandmap.properties: -------------------------------------------------------------------------------- 1 | GET_POPULAR_SUBTHREADS=org.sab.recommendation.commands.GetPopularSubThreads 2 | GET_POPULAR_THREADS=org.sab.recommendation.commands.GetPopularThreads 3 | GET_RECOMMENDED_SUBTHREADS=org.sab.recommendation.commands.GetRecommendedSubThreads 4 | GET_RECOMMENDED_THREADS=org.sab.recommendation.commands.GetRecommendedThreads 5 | GET_RECOMMENDED_USERS=org.sab.recommendation.commands.GetRecommendedUsers 6 | UPDATE_POPULAR_SUBTHREADS=org.sab.recommendation.commands.UpdatePopularSubThreads 7 | UPDATE_POPULAR_THREADS=org.sab.recommendation.commands.UpdatePopularThreads 8 | UPDATE_RECOMMENDED_SUBTHREADS=org.sab.recommendation.commands.UpdateRecommendedSubThreads 9 | UPDATE_RECOMMENDED_THREADS=org.sab.recommendation.commands.UpdateRecommendedThreads 10 | UPDATE_RECOMMENDED_USERS=org.sab.recommendation.commands.UpdateRecommendedUsers 11 | -------------------------------------------------------------------------------- /apps/notification_app/src/main/java/org/sab/notification/FirebaseInitializer.java: -------------------------------------------------------------------------------- 1 | package org.sab.notification; 2 | 3 | import com.google.auth.oauth2.GoogleCredentials; 4 | import com.google.firebase.FirebaseApp; 5 | import com.google.firebase.FirebaseOptions; 6 | 7 | public class FirebaseInitializer { 8 | private static boolean initialized; 9 | 10 | public static boolean initialize() { 11 | try { 12 | if (!initialized) { 13 | FirebaseOptions options = FirebaseOptions.builder() 14 | .setCredentials(GoogleCredentials.getApplicationDefault()).build(); 15 | FirebaseApp.initializeApp(options); 16 | initialized = true; 17 | } 18 | } catch (Exception e) { 19 | e.printStackTrace(); 20 | return false; 21 | } 22 | 23 | return true; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /libs/rabbitmq/src/main/java/org/sab/rabbitmq/SingleBasicChannel.java: -------------------------------------------------------------------------------- 1 | package org.sab.rabbitmq; 2 | 3 | import com.rabbitmq.client.Channel; 4 | 5 | import java.io.IOException; 6 | import java.util.concurrent.TimeoutException; 7 | 8 | abstract class SingleBasicChannel implements AutoCloseable { 9 | protected final Channel channel; 10 | 11 | public SingleBasicChannel(Channel channel) { 12 | this.channel = channel; 13 | } 14 | 15 | protected String declareSpecificQueue(String queue) throws IOException { 16 | final boolean durable = false; 17 | final boolean exclusive = false; 18 | final boolean autoDelete = false; 19 | return channel.queueDeclare(queue, durable, exclusive, autoDelete, null).getQueue(); 20 | } 21 | 22 | 23 | @Override 24 | public void close() throws IOException, TimeoutException { 25 | channel.close(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /libs/postgres/src/main/resources/sql/DropUserProcedures.sql: -------------------------------------------------------------------------------- 1 | DROP FUNCTION IF EXISTS create_user (user_id VARCHAR, 2 | username VARCHAR, 3 | email VARCHAR, 4 | password VARCHAR, 5 | birthdate date, 6 | photo_url VARCHAR); 7 | 8 | 9 | DROP FUNCTION IF EXISTS delete_user(in_username VARCHAR); 10 | 11 | 12 | DROP FUNCTION IF EXISTS update_user_password(in_username VARCHAR, new_password VARCHAR); 13 | 14 | DROP FUNCTION IF EXISTS get_user(in_username VARCHAR); 15 | 16 | 17 | DROP FUNCTION IF EXISTS update_profile_picture(in_username VARCHAR, new_photo_url text); 18 | 19 | 20 | DROP FUNCTION IF EXISTS delete_profile_picture(in_username VARCHAR); 21 | 22 | DROP FUNCTION IF EXISTS is_username_deleted(VARCHAR); 23 | -------------------------------------------------------------------------------- /libs/utilities/src/main/java/org/sab/environmentvariables/EnvVariablesUtils.java: -------------------------------------------------------------------------------- 1 | package org.sab.environmentvariables; 2 | 3 | import java.util.Objects; 4 | 5 | public class EnvVariablesUtils { 6 | 7 | public static String getOrDefaultEnvVariable(String envVariable, String defaultValue) { 8 | String value = System.getenv(envVariable); 9 | return value == null ? defaultValue : value; 10 | } 11 | 12 | /** 13 | * gets an environment variable if exists and throws a null pointer exception if it's not found 14 | * 15 | * @return System.getenv {@code envVariable} 16 | * @throws NullPointerException if {@code envVariable} is not found 17 | */ 18 | public static String getEnvOrThrow(String envVariable) { 19 | return Objects.requireNonNull(System.getenv(envVariable), String.format("The Environment Variable \"%s\" cannot be null", envVariable)); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /libs/models/src/main/java/org/sab/models/report/SubThreadReportAttributes.java: -------------------------------------------------------------------------------- 1 | package org.sab.models.report; 2 | 3 | public enum SubThreadReportAttributes { 4 | 5 | // variables in a Report object 6 | Report_Id("id", "Id"), 7 | 8 | 9 | SUBTHREAD_Id("subthreadId", "SubthreadId"), 10 | DATE_CREATED("dateCreated", "DateCreated"), 11 | 12 | REPORTER_ID("userId", "UserId"), 13 | TYPE_OF_REPORT ("typeOfReport", "TypeOfReport"), 14 | 15 | 16 | PARENT_THREAD_ID("threadId", "ThreadId"), 17 | REPORT_MSG("reportMsg", "ReportMessage"); 18 | 19 | 20 | private final String http; 21 | private final String db; 22 | 23 | SubThreadReportAttributes(String http, String db) { 24 | this.http = http; 25 | this.db = db; 26 | } 27 | 28 | public String getHTTP() { 29 | return http; 30 | } 31 | 32 | public String getDb() { 33 | return db; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /apps/example_app/src/main/java/org/sab/demo/commands/CommandNeedingClaims.java: -------------------------------------------------------------------------------- 1 | package org.sab.demo.commands; 2 | 3 | import org.sab.service.Responder; 4 | import org.sab.service.validation.CommandWithVerification; 5 | import org.sab.service.validation.HTTPMethod; 6 | import org.sab.validation.Schema; 7 | 8 | public class CommandNeedingClaims extends CommandWithVerification { 9 | 10 | @Override 11 | protected String execute() { 12 | String username = authenticationParams.getString("username"); 13 | return Responder.makeMsgResponse(String.format("Hello %s!", username)); 14 | } 15 | 16 | @Override 17 | protected Schema getSchema() { 18 | return Schema.emptySchema(); 19 | } 20 | 21 | @Override 22 | protected HTTPMethod getMethodType() { 23 | return HTTPMethod.GET; 24 | } 25 | 26 | @Override 27 | protected boolean isAuthNeeded(){ 28 | return true; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cli/components/message.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const { Text, Box, Spacer } = require('ink') 3 | const { useContext } = require('react') 4 | const AppContext = require('../contexts/app-context') 5 | 6 | const moment = require('moment') 7 | const { mapIdToSpecialId } = require('../utils/id-mapper') 8 | 9 | const Message = ({ content, senderId, timestamp }) => { 10 | const { userId } = useContext(AppContext) 11 | const isYourMessage = senderId === userId 12 | return ( 13 | 14 | 15 | 16 | {(isYourMessage ? 'You' : mapIdToSpecialId(senderId)) + ': '} 17 | 18 | 19 | 20 | {content} 21 | 22 | 23 | 24 | {` (${moment(timestamp).calendar()})`} 25 | 26 | 27 | ) 28 | } 29 | 30 | module.exports = Message 31 | -------------------------------------------------------------------------------- /apps/search_app/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | rabbitmq: 4 | image: "rabbitmq:management" 5 | ports: 6 | - 15672:15672 7 | arangodb: 8 | image: "arangodb" 9 | ports: 10 | - 8529:8529 11 | volumes: 12 | - arangodb_data:/var/lib/arangodb3 13 | - arangodb_apps_data:/var/lib/arangodb3-apps 14 | environment: 15 | - ARANGO_ROOT_PASSWORD=root 16 | search_app: 17 | build: 18 | context: "../../" 19 | dockerfile: "apps/search_app/Dockerfile" 20 | restart: on-failure 21 | ports: 22 | - "4005:4005" 23 | environment: 24 | - RABBIT_HOST=rabbitmq 25 | - ARANGO_DB=Reddit 26 | - ARANGO_USER=root 27 | - ARANGO_PASSWORD=root 28 | - ARANGO_HOST=arangodb 29 | depends_on: 30 | - "arangodb" 31 | - "rabbitmq" 32 | 33 | networks: 34 | default: 35 | name: search_nw 36 | 37 | volumes: 38 | arangodb_data: 39 | arangodb_apps_data: 40 | -------------------------------------------------------------------------------- /libs/utilities/src/main/java/org/sab/validation/Attribute.java: -------------------------------------------------------------------------------- 1 | package org.sab.validation; 2 | 3 | public class Attribute { 4 | 5 | String attributeName; 6 | DataType dataType; 7 | boolean isRequired = false; 8 | 9 | public String getAttributeName() { 10 | return attributeName; 11 | } 12 | 13 | public DataType getDataType() { 14 | return dataType; 15 | } 16 | 17 | public boolean isRequired() { 18 | return isRequired; 19 | } 20 | 21 | public Attribute(String attributeName, DataType dataType) { 22 | this(attributeName, dataType, false); 23 | } 24 | 25 | public Attribute(String attributeName, DataType dataType, boolean isRequired) { 26 | this.attributeName = attributeName; 27 | this.dataType = dataType; 28 | this.isRequired = isRequired; 29 | } 30 | 31 | public boolean isValidlyTyped(Object object) { 32 | return dataType.isOfValidType(object); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /libs/models/src/main/java/org/sab/models/ThreadAttributes.java: -------------------------------------------------------------------------------- 1 | package org.sab.models; 2 | 3 | public enum ThreadAttributes { 4 | // variables in a thread object 5 | THREAD_NAME("name", "Name"), 6 | DESCRIPTION("description", "Description"), 7 | CREATOR_ID("creatorId", "CreatorId"), 8 | NUM_OF_FOLLOWERS("numOfFollowers", "NumOfFollowers"), 9 | DATE_CREATED("dateCreated", "DateCreated"), 10 | 11 | // additional request parameters 12 | ASSIGNER_ID("assignerId", null), 13 | MODERATOR_ID("moderatorId", null), 14 | ACTION_MAKER_ID("userId", "UserId"), 15 | BANNED_USER_ID("bannedUserId", "BannedUserId"); 16 | 17 | 18 | private final String http; 19 | private final String db; 20 | 21 | ThreadAttributes(String http, String db) { 22 | this.http = http; 23 | this.db = db; 24 | } 25 | 26 | public String getHTTP() { 27 | return http; 28 | } 29 | 30 | public String getDb() { 31 | return db; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /libs/models/src/main/java/org/sab/models/SubThreadAttributes.java: -------------------------------------------------------------------------------- 1 | package org.sab.models; 2 | 3 | public enum SubThreadAttributes { 4 | 5 | // variables in a subthread object 6 | SUBTHREAD_ID("id", "Id"), 7 | PARENT_THREAD_ID("parentThreadId", "ParentThreadId"), 8 | CREATOR_ID("creatorId", "CreatorId"), 9 | 10 | TITLE("title", "Title"), 11 | CONTENT("content", "Content"), 12 | 13 | LIKES("likes", "Likes"), 14 | DISLIKES("dislikes", "Dislikes"), 15 | 16 | HAS_IMAGE("hasImage", "HasImage"), 17 | 18 | DATE_CREATED("dateCreated", "DateCreated"), 19 | 20 | // additional attributes 21 | ACTION_MAKER_ID("userId", null); 22 | 23 | private final String http; 24 | private final String db; 25 | 26 | SubThreadAttributes(String http, String db) { 27 | this.http = http; 28 | this.db = db; 29 | } 30 | 31 | public String getHTTP() { 32 | return http; 33 | } 34 | 35 | public String getDb() { 36 | return db; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /libs/utilities/src/main/java/org/sab/io/IoUtils.java: -------------------------------------------------------------------------------- 1 | package org.sab.io; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.handler.codec.base64.Base64; 5 | 6 | import java.io.ByteArrayInputStream; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.nio.charset.StandardCharsets; 10 | 11 | import static io.netty.buffer.Unpooled.copiedBuffer; 12 | 13 | public class IoUtils { 14 | 15 | public static String encodeFile(ByteBuf byteBuf) { 16 | return Base64.encode(byteBuf).toString(StandardCharsets.UTF_8); 17 | } 18 | 19 | public static String encodeFile(InputStream inputStream) throws IOException { 20 | byte[] bytes = inputStream.readAllBytes(); 21 | return encodeFile(copiedBuffer(bytes)); 22 | } 23 | 24 | public static InputStream decodeFile(String encodedFile) { 25 | byte[] bytes = org.apache.commons.codec.binary.Base64.decodeBase64(encodedFile); 26 | InputStream stream = new ByteArrayInputStream(bytes); 27 | return stream; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /libs/rabbitmq/src/main/java/org/sab/rabbitmq/RPCClient.java: -------------------------------------------------------------------------------- 1 | package org.sab.rabbitmq; 2 | 3 | import java.io.IOException; 4 | import java.util.concurrent.TimeoutException; 5 | 6 | import com.rabbitmq.client.Connection; 7 | 8 | public class RPCClient implements AutoCloseable { 9 | 10 | private static RPCClient instance = null; 11 | private final Connection connection; 12 | 13 | // creating a connection with RabbitMQ 14 | private RPCClient() throws IOException, TimeoutException { 15 | super(); 16 | connection = RPCBase.initConnection(); 17 | } 18 | 19 | public static SingleClientChannel getSingleChannelExecutor() throws IOException, TimeoutException { 20 | if (instance == null) { 21 | instance = new RPCClient(); 22 | } 23 | 24 | return new SingleClientChannel(instance.connection.createChannel()); 25 | } 26 | 27 | // Close the |channel| as to not waste resources. 28 | public void close() throws IOException { 29 | connection.close(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /libs/models/src/main/java/org/sab/models/CommentAttributes.java: -------------------------------------------------------------------------------- 1 | package org.sab.models; 2 | 3 | public enum CommentAttributes { 4 | 5 | // variables in a Comment object 6 | COMMENT_ID("commentId", "CommentId"), 7 | CREATOR_ID("creatorId", "CreatorId"), 8 | 9 | // TODO parent could be a comment... 10 | PARENT_SUBTHREAD_ID("parentSubthreadId", "ParentSubthreadId"), 11 | PARENT_CONTENT_TYPE("parentContentType", "ParentContentType"), 12 | 13 | CONTENT("content", "Content"), 14 | DATE_CREATED("dateCreated", "DateCreated"), 15 | 16 | LIKES("likes", "Likes"), 17 | DISLIKES("dislikes", "Dislikes"), 18 | 19 | // additional request parameters 20 | ACTION_MAKER_ID("userId", null); 21 | 22 | private final String http; 23 | private final String db; 24 | 25 | CommentAttributes(String http, String db) { 26 | this.http = http; 27 | this.db = db; 28 | } 29 | 30 | public String getHTTP() { 31 | return http; 32 | } 33 | 34 | public String getDb() { 35 | return db; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /libs/service/src/main/java/org/sab/service/ServiceConstants.java: -------------------------------------------------------------------------------- 1 | package org.sab.service; 2 | 3 | import java.util.ArrayList; 4 | 5 | public class ServiceConstants { 6 | 7 | private ServiceConstants () { 8 | } 9 | 10 | public static final String COMMAND_MAP_FILENAME = "commandMap.properties".toLowerCase(); 11 | 12 | public static final String REQUEST_QUEUE_NAME_SUFFIX = "_REQ"; 13 | public static final int MAX_THREAD_TIMEOUT = 4; 14 | 15 | public static final String THREADS_COUNT_PROPERTY_NAME = "threadsCount"; 16 | public static final int DEFAULT_THREADS_COUNT = 10; 17 | 18 | public static final String REQUIRED_DATABASES_PROPERTY_NAME = "requiredDatabases"; 19 | public static final String REQUIRED_DATABASES_ARRAY_DELIMITER = ","; 20 | public static final String REQUIRED_DATABASES_PAIR_DELIMITER = "-"; 21 | public static final ArrayList DEFAULT_REQUIRED_DATABASES = new ArrayList<>(); 22 | 23 | public static final int DEFAULT_CONNECTION_COUNT = 10; 24 | 25 | public static final String GET_DB_CLIENT_METHOD_NAME = "getInstance"; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /apps/chat_app/README.md: -------------------------------------------------------------------------------- 1 | # Chat App 2 | 3 | The chatting application of Reddit. 4 | 5 | ## Installing and Running Dependencies 6 | 7 | You must have docker installed for running the next commands. 8 | 9 | ### Install and Run Cassandra 10 | 11 | ```bash 12 | docker run -d \ -p 9042:9042 \ --name cassandra \ cassandra:latest 13 | ``` 14 | 15 | #### Install and Run Cassandra Web (optional) 16 | 17 | If you want to view the chat database through online UI 18 | ```bash 19 | docker run -d \ 20 | -e CASSANDRA_HOST_IP=$(docker inspect --format '{{.NetworkSettings.IPAddress}}' cassandra) \ 21 | -e CASSANDRA_PORT=9042 \ 22 | -p 3000:3000 \ 23 | --name cassandra-web \ 24 | delermando/docker-cassandra-web:v0.4.0 25 | ``` 26 | You can view the UI on `http://localhost:3000/`. 27 | 28 | ### Install and Run RabbitMQ 29 | 30 | ```bash 31 | docker run -d -p 5672:5672 -p 15672:15672 rabbitmq:management 32 | ``` 33 | 34 | ## Running Chat App 35 | 36 | Now all you have to do is run the `ChatServer` class in `chat_server` module and `ChatStorageApp` class in `chat_storage` module, then enjoy using the chatting app. 37 | 38 | -------------------------------------------------------------------------------- /apps/chat_app/chat_server/src/main/java/org/sab/chat/server/routers/RouterBuilder.java: -------------------------------------------------------------------------------- 1 | package org.sab.chat.server.routers; 2 | 3 | public class RouterBuilder { 4 | 5 | public static Router createRouter(String routerType) { 6 | return switch (routerType) { 7 | case "INIT_CONNECTION" -> new InitConnectionRouter(); 8 | case "ADD_GROUP_MEMBER" -> new AddMemberRouter(); 9 | case "REMOVE_GROUP_MEMBER" -> new RemoveMemberRouter(); 10 | case "LEAVE_GROUP" -> new LeaveGroupRouter(); 11 | case "GET_CHATS" -> new GetChatsRouter(); 12 | case "GET_DIRECT_MESSAGES" -> new GetDirectMessagesRouter(); 13 | case "GET_GROUP_MESSAGES" -> new GetGroupMessagesRouter(); 14 | case "CREATE_GROUP_MESSAGE" -> new CreateGroupMessageRouter(); 15 | case "CREATE_GROUP_CHAT" -> new CreateGroupChatRouter(); 16 | case "CREATE_DIRECT_CHAT" -> new CreateDirectChatRouter(); 17 | case "CREATE_DIRECT_MESSAGE" -> new CreateDirectMessageRouter(); 18 | default -> null; 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/chat_app/chat_storage/src/main/java/org/sab/chat/storage/tables/TableUtils.java: -------------------------------------------------------------------------------- 1 | package org.sab.chat.storage.tables; 2 | 3 | import com.datastax.driver.core.Row; 4 | 5 | import java.time.Instant; 6 | import java.util.UUID; 7 | import java.util.List; 8 | 9 | public class TableUtils { 10 | public static boolean isEmpty(List rows) { 11 | return rows == null || rows.size() == 0; 12 | } 13 | 14 | private static final long NUM_HUNDRED_NANOS_IN_A_SECOND = 10_000_000L; 15 | 16 | private static final long NUM_HUNDRED_NANOS_FROM_UUID_EPOCH_TO_UNIX_EPOCH = 122_192_928_000_000_000L; 17 | 18 | public static Instant getInstantFromUUID(final UUID uuid) { 19 | final long hundredNanosSinceUnixEpoch = uuid.timestamp() - NUM_HUNDRED_NANOS_FROM_UUID_EPOCH_TO_UNIX_EPOCH; 20 | final long secondsSinceUnixEpoch = hundredNanosSinceUnixEpoch / NUM_HUNDRED_NANOS_IN_A_SECOND; 21 | final long nanoAdjustment = ((hundredNanosSinceUnixEpoch % NUM_HUNDRED_NANOS_IN_A_SECOND) * 100); 22 | return Instant.ofEpochSecond(secondsSinceUnixEpoch, nanoAdjustment); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cli/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | process.env.NODE_ENV = 'production'; 4 | 5 | const React = require('react') 6 | const importJsx = require('import-jsx') 7 | const { render } = require('ink') 8 | const meow = require('meow') 9 | 10 | const App = importJsx('./app') 11 | 12 | 13 | const cli = meow( 14 | ` 15 | Usage 16 | $ reddit-cli 17 | 18 | Commands 19 | $ reddit-cli welcome 20 | $ reddit-cli chat 21 | 22 | Flags 23 | --user, -u specify the user id. Special user ids are joe, ouda, abu and ronic. 24 | --rainbow, -r Add rainbow welcome title 25 | 26 | Examples 27 | $ reddit-cli chat -u ee55dcf8-ee7b-429a-939e-12c2f7b7ddee 28 | $ reddit-cli chat -u abu 29 | $ reddit-cli welcome -r 30 | `, 31 | { 32 | flags: { 33 | user: { 34 | type: 'string', 35 | alias: 'u' 36 | }, 37 | rainbow: { 38 | type: 'boolean', 39 | alias: 'r' 40 | } 41 | } 42 | } 43 | ) 44 | 45 | render( 46 | React.createElement(App, { 47 | command: cli.input[0], 48 | flags: { 49 | user: cli.flags.user, 50 | rainbow: cli.flags.rainbow 51 | } 52 | }) 53 | ) 54 | -------------------------------------------------------------------------------- /libs/utilities/src/main/java/org/sab/databases/PooledDatabaseClient.java: -------------------------------------------------------------------------------- 1 | package org.sab.databases; 2 | 3 | /** 4 | * This class assumes that the DB client is following a singleton pattern 5 | * However there is no clean way to force the inheritance of singletons 6 | * 7 | * Thus, we assume that the concept of "consenting adults" applies and 8 | * that the concrete classes will follow without an explicit contract 9 | * 10 | * Check commented compulsory method below 11 | * */ 12 | public interface PooledDatabaseClient { 13 | 14 | // Note: a decorator pattern would be a better design strategy 15 | void createPool(int maxConnections); 16 | void destroyPool() throws PoolDoesNotExistException; 17 | void setMaxConnections(int maxConnections) throws PoolDoesNotExistException; 18 | String getName(); 19 | 20 | /** 21 | * Compulsory method! 22 | * Every concrete class that implements this interface needs to implement this method 23 | * 24 | * ConcreteClass should be replaced by the DB client type Ex. Arango 25 | * */ 26 | // public static ConcreteClass getInstance() 27 | 28 | } 29 | -------------------------------------------------------------------------------- /libs/utilities/src/main/java/org/sab/strings/StringManipulation.java: -------------------------------------------------------------------------------- 1 | package org.sab.strings; 2 | 3 | public class StringManipulation { 4 | private StringManipulation() { 5 | } 6 | 7 | public static String camelToPascalCase(String camelCase) { 8 | char[] copy = camelCase.toCharArray(); 9 | copy[0] = (copy[0] + "").toUpperCase().charAt(0); 10 | return new String(copy); 11 | } 12 | 13 | public static String camelToSnakeCase(String camelcase) { 14 | StringBuilder snakeCase = new StringBuilder(); 15 | for (char c : camelcase.toCharArray()) 16 | if (isCapital(c)) 17 | snakeCase.append("_").append((c + "").toLowerCase()); 18 | else 19 | snakeCase.append(c); 20 | return snakeCase.toString(); 21 | 22 | } 23 | 24 | public static String pascalToScreamingCase(String pascalCase) { 25 | return camelToSnakeCase((pascalCase.charAt(0) + "").toLowerCase() + pascalCase.substring(1)); 26 | } 27 | 28 | public static boolean isCapital(char c) { 29 | return c >= 'A' && c <= 'Z'; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /libs/service/src/main/java/org/sab/service/Responder.java: -------------------------------------------------------------------------------- 1 | package org.sab.service; 2 | 3 | import org.json.JSONArray; 4 | import org.json.JSONObject; 5 | 6 | public class Responder { 7 | private Responder() { 8 | } 9 | 10 | public static String makeErrorResponse(String msg, int statusCode) { 11 | JSONObject error = new JSONObject().put("msg", msg).put("statusCode", statusCode); 12 | return error.toString(); 13 | } 14 | 15 | public static String makeDataResponse(JSONObject data) { 16 | JSONObject response = new JSONObject().put("data", data); 17 | response.put("statusCode", 200); 18 | return response.toString(); 19 | } 20 | 21 | public static String makeMsgResponse(String msg) { 22 | JSONObject response = new JSONObject().put("msg", msg); 23 | response.put("statusCode", 200); 24 | return response.toString(); 25 | } 26 | 27 | public static JSONObject makeDataResponse(JSONArray data) { 28 | JSONObject response = new JSONObject().put("data", data); 29 | response.put("statusCode", 200); 30 | return response; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/chat_app/chat_server/src/main/java/org/sab/chat/server/routers/GetGroupMessagesRouter.java: -------------------------------------------------------------------------------- 1 | package org.sab.chat.server.routers; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import org.json.simple.JSONObject; 5 | import org.sab.chat.server.ClientManager; 6 | 7 | public class GetGroupMessagesRouter extends Router { 8 | 9 | @Override 10 | public void forwardRequestToQueue(ChannelHandlerContext ctx, JSONObject request) { 11 | boolean isAuthenticated = authenticate(request, "userId"); 12 | if (!isAuthenticated) { 13 | rejectUnAuthenticatedRequest(ctx); 14 | return; 15 | } 16 | String[] attributes = {"chatId", "userId"}; 17 | packAndForwardRequest(ctx, request, attributes); 18 | } 19 | 20 | @Override 21 | public void routeResponse(ChannelHandlerContext ctx, JSONObject response) { 22 | JSONObject data = (JSONObject) response.get("data"); 23 | if(data == null) { 24 | handleError(ctx, response); 25 | return; 26 | } 27 | 28 | ClientManager.sendResponseToChannel(ctx.channel(), response); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /libs/service/src/main/java/org/sab/service/managers/ThreadPoolManager.java: -------------------------------------------------------------------------------- 1 | package org.sab.service.managers; 2 | 3 | import org.sab.service.ServiceConstants; 4 | 5 | import java.util.concurrent.*; 6 | 7 | public class ThreadPoolManager { 8 | private ExecutorService threadPool = null; 9 | 10 | public void initThreadPool(int threadCount) { 11 | threadPool = Executors.newFixedThreadPool(threadCount); 12 | } 13 | 14 | public void releaseThreadPool() { 15 | if (threadPool == null) { 16 | return; 17 | } 18 | 19 | try { 20 | threadPool.shutdown(); 21 | if (!threadPool.awaitTermination(ServiceConstants.MAX_THREAD_TIMEOUT, TimeUnit.MINUTES)) { 22 | threadPool.shutdownNow(); 23 | } 24 | } catch (InterruptedException e) { 25 | threadPool.shutdownNow(); 26 | } 27 | } 28 | 29 | public void dispose() { 30 | releaseThreadPool(); 31 | threadPool = null; 32 | } 33 | 34 | public Future submit(Callable callable) { 35 | return threadPool.submit(callable); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /notification-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.14.1", 8 | "@testing-library/react": "^11.2.7", 9 | "@testing-library/user-event": "^12.8.3", 10 | "axios": "^0.21.1", 11 | "bootstrap": "^5.0.2", 12 | "firebase": "^8.6.8", 13 | "react": "^17.0.2", 14 | "react-bootstrap": "^1.6.1", 15 | "react-dom": "^17.0.2", 16 | "react-scripts": "4.0.3", 17 | "web-vitals": "^1.1.2" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": [ 27 | "react-app", 28 | "react-app/jest" 29 | ] 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /apps/chat_app/chat_server/src/main/java/org/sab/chat/server/routers/GetChatsRouter.java: -------------------------------------------------------------------------------- 1 | package org.sab.chat.server.routers; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import org.json.simple.JSONObject; 5 | import org.sab.chat.server.ClientManager; 6 | 7 | import java.util.UUID; 8 | 9 | public class GetChatsRouter extends Router { 10 | 11 | @Override 12 | public void forwardRequestToQueue(ChannelHandlerContext ctx, JSONObject request) { 13 | boolean isAuthenticated = authenticate(request, "userId"); 14 | if (!isAuthenticated) { 15 | rejectUnAuthenticatedRequest(ctx); 16 | return; 17 | } 18 | String[] attributes = {"userId"}; 19 | packAndForwardRequest(ctx, request, attributes); 20 | } 21 | 22 | @Override 23 | public void routeResponse(ChannelHandlerContext ctx, JSONObject response) { 24 | JSONObject data = (JSONObject) response.get("data"); 25 | if(data == null) { 26 | handleError(ctx, response); 27 | return; 28 | } 29 | 30 | ClientManager.sendResponseToChannel(ctx.channel(), response); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/chat_app/chat_server/src/main/java/org/sab/chat/server/routers/GetDirectMessagesRouter.java: -------------------------------------------------------------------------------- 1 | package org.sab.chat.server.routers; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import org.json.simple.JSONObject; 5 | import org.sab.chat.server.ClientManager; 6 | 7 | import java.util.UUID; 8 | 9 | public class GetDirectMessagesRouter extends Router { 10 | 11 | @Override 12 | public void forwardRequestToQueue(ChannelHandlerContext ctx, JSONObject request) { 13 | boolean isAuthenticated = authenticate(request, "userId"); 14 | if (!isAuthenticated) { 15 | rejectUnAuthenticatedRequest(ctx); 16 | return; 17 | } 18 | String[] attributes = {"chatId", "userId"}; 19 | packAndForwardRequest(ctx, request, attributes); 20 | } 21 | 22 | @Override 23 | public void routeResponse(ChannelHandlerContext ctx, JSONObject response) { 24 | JSONObject data = (JSONObject) response.get("data"); 25 | if(data == null) { 26 | handleError(ctx, response); 27 | return; 28 | } 29 | 30 | ClientManager.sendResponseToChannel(ctx.channel(), response); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /libs/utilities/src/main/java/org/sab/tests/TestsUtils.java: -------------------------------------------------------------------------------- 1 | package org.sab.tests; 2 | 3 | import org.json.JSONObject; 4 | import org.sab.auth.AuthParamsHandler; 5 | 6 | import java.util.Map; 7 | 8 | public class TestsUtils { 9 | public static JSONObject makeRequest(JSONObject body, String methodType, JSONObject uriParams) { 10 | JSONObject request = new JSONObject(); 11 | request.put("body", body); 12 | request.put("methodType", methodType); 13 | request.put("uriParams", uriParams); 14 | return request; 15 | } 16 | 17 | public static JSONObject makeAuthorizedRequest(JSONObject body, String methodType, JSONObject uriParams) { 18 | JSONObject unAuthorizedRequest = makeRequest(body, methodType, uriParams); 19 | return AuthParamsHandler.putAuthorizedParams(unAuthorizedRequest); 20 | } 21 | 22 | public static JSONObject makeAuthorizedRequest(JSONObject body, String methodType, JSONObject uriParams, String username) { 23 | JSONObject unAuthorizedRequest = makeRequest(body, methodType, uriParams); 24 | return AuthParamsHandler.putAuthorizedParams(unAuthorizedRequest, Map.of("username", username)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/thread_app/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | rabbitmq: 4 | image: "rabbitmq:management" 5 | ports: 6 | - "15672:15672" 7 | - "5672:5672" 8 | arangodb: 9 | image: "arangodb" 10 | env_file: 11 | - "../../.env" 12 | ports: 13 | - "8529:8529" 14 | couchbase: 15 | build: 16 | context: "../../" 17 | dockerfile: "libs/couchbase/Dockerfile" 18 | environment: 19 | - COUCHBASE_ADMINISTRATOR_USERNAME=Administrator 20 | - COUCHBASE_ADMINISTRATOR_PASSWORD=password 21 | - COUCHBASE_CLUSTER_NAME=Reddit 22 | - COUCHBASE_RAM=1024 23 | ports: 24 | - 8091-8094:8091-8094 25 | - 11210:11210 26 | thread_app: 27 | build: 28 | context: "../../" 29 | dockerfile: "apps/thread_app/Dockerfile" 30 | restart: on-failure 31 | ports: 32 | - "4007:4007" 33 | environment: 34 | - ARANGO_HOST=arangodb 35 | - COUCHBASE_HOST=couchbase 36 | - RABBIT_HOST=rabbitmq 37 | env_file: 38 | - "../../.env" 39 | 40 | depends_on: 41 | - "rabbitmq" 42 | - "arangodb" 43 | - "couchbase" 44 | 45 | networks: 46 | default: 47 | name: thread_nw 48 | -------------------------------------------------------------------------------- /apps/subthread_app/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | rabbitmq: 4 | image: "rabbitmq:management" 5 | ports: 6 | - "15672:15672" 7 | - "5672:5672" 8 | arangodb: 9 | image: "arangodb" 10 | env_file: 11 | - "../../.env" 12 | ports: 13 | - "8529:8529" 14 | couchbase: 15 | build: 16 | context: "../../" 17 | dockerfile: "libs/couchbase/Dockerfile" 18 | environment: 19 | - COUCHBASE_ADMINISTRATOR_USERNAME=Administrator 20 | - COUCHBASE_ADMINISTRATOR_PASSWORD=password 21 | - COUCHBASE_CLUSTER_NAME=Reddit 22 | - COUCHBASE_RAM=1024 23 | ports: 24 | - 8091-8094:8091-8094 25 | - 11210:11210 26 | subthread_app: 27 | build: 28 | context: "../../" 29 | dockerfile: "apps/subthread_app/Dockerfile" 30 | restart: on-failure 31 | ports: 32 | - "4006:4006" 33 | environment: 34 | - ARANGO_HOST=arangodb 35 | - COUCHBASE_HOST=couchbase 36 | - RABBIT_HOST=rabbitmq 37 | env_file: 38 | - "../../.env" 39 | 40 | depends_on: 41 | - "rabbitmq" 42 | - "arangodb" 43 | - "couchbase" 44 | 45 | networks: 46 | default: 47 | name: subthread_nw 48 | -------------------------------------------------------------------------------- /apps/chat_app/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | rabbitmq: 4 | image: "rabbitmq:management" 5 | ports: 6 | - "15672:15672" 7 | cassandra: 8 | image: "cassandra" 9 | chat_server: 10 | build: 11 | context: "../../" 12 | dockerfile: "apps/chat_app/chat_server/Dockerfile" 13 | env_file: 14 | - .env 15 | environment: 16 | - RABBIT_HOST=rabbitmq 17 | ports: 18 | - "5000:5000" 19 | depends_on: 20 | - "rabbitmq" 21 | chat_storage: 22 | build: 23 | context: "../../" 24 | dockerfile: "apps/chat_app/chat_storage/Dockerfile" 25 | restart: on-failure 26 | ports: 27 | - "4001:4001" 28 | environment: 29 | - RABBIT_HOST=rabbitmq 30 | - CASSANDRA_NODE=cassandra 31 | - CASSANDRA_PORT=9042 32 | - KEYSPACE_NAME=chat_app 33 | - REPLICATION_STRATEGY=SimpleStrategy 34 | - REPLICATION_FACTOR=2 35 | depends_on: 36 | - "cassandra" 37 | - "rabbitmq" 38 | 39 | networks: 40 | default: 41 | name: chat_nw 42 | -------------------------------------------------------------------------------- /libs/service/src/main/java/org/sab/service/databases/DBConfig.java: -------------------------------------------------------------------------------- 1 | package org.sab.service.databases; 2 | 3 | import org.sab.databases.PooledDatabaseClient; 4 | 5 | public class DBConfig { 6 | private int connectionCount; 7 | private PooledDatabaseClient client; 8 | 9 | public DBConfig(int connectionCount) { 10 | this.connectionCount = connectionCount; 11 | } 12 | 13 | public String getClientName() { 14 | return client == null ? "No client initialized yet" : client.getName(); 15 | } 16 | 17 | public int getConnectionCount() { 18 | return connectionCount; 19 | } 20 | 21 | public void setConnectionCount(int connectionCount) { 22 | this.connectionCount = connectionCount; 23 | } 24 | 25 | public PooledDatabaseClient getClient() { 26 | return client; 27 | } 28 | 29 | public void setClient(PooledDatabaseClient client) { 30 | this.client = client; 31 | } 32 | 33 | @Override 34 | public String toString() { 35 | return "DBConfig{" + 36 | "name='" + getClientName() + '\'' + 37 | ", connectionCount=" + connectionCount + 38 | ", dbClient=" + client + 39 | '}'; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /libs/rabbitmq/src/main/java/org/sab/rabbitmq/RPCServer.java: -------------------------------------------------------------------------------- 1 | package org.sab.rabbitmq; 2 | 3 | import com.rabbitmq.client.Connection; 4 | import org.json.JSONObject; 5 | import org.sab.functions.TriFunction; 6 | 7 | import java.io.IOException; 8 | import java.util.concurrent.TimeoutException; 9 | 10 | public class RPCServer { 11 | private static RPCServer instance = null; 12 | private final Connection connection; 13 | 14 | // creating a connection with RabbitMQ 15 | private RPCServer() throws IOException, TimeoutException { 16 | super(); 17 | connection = RPCBase.initConnection(); 18 | } 19 | 20 | public static SingleServerChannel getSingleChannelExecutor(String queueName, 21 | TriFunction executor) 22 | throws IOException, TimeoutException { 23 | if (instance == null) { 24 | instance = new RPCServer(); 25 | } 26 | 27 | return new SingleServerChannel(instance.connection.createChannel(), queueName, executor); 28 | } 29 | 30 | // Close the |channel| as to not waste resources. 31 | public void close() throws IOException { 32 | connection.close(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /apps/controller/src/main/java/org/sab/controller/ControllerClientHandler.java: -------------------------------------------------------------------------------- 1 | package org.sab.controller; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.buffer.Unpooled; 5 | import io.netty.channel.ChannelHandler; 6 | import io.netty.channel.ChannelHandlerContext; 7 | import io.netty.channel.SimpleChannelInboundHandler; 8 | import io.netty.util.CharsetUtil; 9 | 10 | @ChannelHandler.Sharable 11 | public class ControllerClientHandler extends SimpleChannelInboundHandler { 12 | 13 | private final String cmd; 14 | 15 | ControllerClientHandler(String cmd) { 16 | this.cmd = cmd; 17 | } 18 | 19 | @Override 20 | public void channelActive(ChannelHandlerContext ctx) { 21 | System.out.printf("Channel %s is active\n", ctx.channel().id()); 22 | ctx.writeAndFlush(Unpooled.copiedBuffer(cmd, CharsetUtil.UTF_8)); 23 | 24 | 25 | } 26 | 27 | @Override 28 | public void channelRead0(ChannelHandlerContext ctx, ByteBuf in) { 29 | throw new UnsupportedOperationException("Controller shouldn't receive anything"); 30 | } 31 | 32 | @Override 33 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 34 | cause.printStackTrace(); 35 | ctx.close(); 36 | } 37 | } -------------------------------------------------------------------------------- /apps/chat_app/chat_server/src/main/java/org/sab/chat/server/routers/CreateGroupMessageRouter.java: -------------------------------------------------------------------------------- 1 | package org.sab.chat.server.routers; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import org.json.simple.JSONObject; 5 | import org.sab.chat.server.ClientManager; 6 | 7 | import java.util.UUID; 8 | 9 | public class CreateGroupMessageRouter extends Router { 10 | 11 | @Override 12 | public void forwardRequestToQueue(ChannelHandlerContext ctx, JSONObject request) { 13 | boolean isAuthenticated = authenticate(request, "senderId"); 14 | if (!isAuthenticated) { 15 | rejectUnAuthenticatedRequest(ctx); 16 | return; 17 | } 18 | String[] attributes = {"chatId", "senderId", "content"}; 19 | packAndForwardRequest(ctx, request, attributes); 20 | } 21 | 22 | @Override 23 | public void routeResponse(ChannelHandlerContext ctx, JSONObject response) { 24 | JSONObject data = (JSONObject) response.get("data"); 25 | if(data == null) { 26 | handleError(ctx, response); 27 | return; 28 | } 29 | 30 | UUID chatId = UUID.fromString((String) data.get("chatId")); 31 | 32 | ClientManager.broadcastResponseToChatChannels(chatId, response); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/chat_app/chat_server/src/main/java/org/sab/chat/server/routers/CreateDirectMessageRouter.java: -------------------------------------------------------------------------------- 1 | package org.sab.chat.server.routers; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import org.json.simple.JSONObject; 5 | import org.sab.chat.server.ClientManager; 6 | 7 | import java.util.UUID; 8 | 9 | public class CreateDirectMessageRouter extends Router { 10 | 11 | @Override 12 | public void forwardRequestToQueue(ChannelHandlerContext ctx, JSONObject request) { 13 | boolean isAuthenticated = authenticate(request, "senderId"); 14 | if (!isAuthenticated) { 15 | rejectUnAuthenticatedRequest(ctx); 16 | return; 17 | } 18 | String[] attributes = {"chatId", "senderId", "content"}; 19 | packAndForwardRequest(ctx, request, attributes); 20 | } 21 | 22 | @Override 23 | public void routeResponse(ChannelHandlerContext ctx, JSONObject response) { 24 | JSONObject data = (JSONObject) response.get("data"); 25 | if(data == null) { 26 | handleError(ctx, response); 27 | return; 28 | } 29 | 30 | UUID chatId = UUID.fromString((String) data.get("chatId")); 31 | 32 | ClientManager.broadcastResponseToChatChannels(chatId, response); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /libs/utilities/src/main/java/org/sab/HttpServerUtilities/HttpClient.java: -------------------------------------------------------------------------------- 1 | package org.sab.HttpServerUtilities; 2 | 3 | import java.io.IOException; 4 | import java.net.URI; 5 | import java.net.http.HttpRequest; 6 | import java.net.http.HttpResponse; 7 | 8 | public class HttpClient { 9 | /** 10 | * Makes a getRequest to server deployed at localhost:8080 11 | * 12 | * @param uri assumes the uri is of the form api/appName (e.g api/user) 13 | */ 14 | public static String get(String uri, String functionName) throws IOException, InterruptedException { 15 | return get(uri, functionName, 8080); 16 | } 17 | 18 | public static String get(String uri, String functionName, int port) throws IOException, InterruptedException { 19 | uri = String.format("http://localhost:%d/", port) + uri; 20 | java.net.http.HttpClient client = java.net.http.HttpClient.newHttpClient(); 21 | HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri)) 22 | .setHeader("Function-Name", functionName) 23 | .setHeader("Content-Type", "application/json") 24 | .build(); 25 | 26 | HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); 27 | return response.body(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /notification-client/public/firebase-messaging-sw.js: -------------------------------------------------------------------------------- 1 | // Scripts for firebase and firebase messaging 2 | importScripts('https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js'); 3 | importScripts('https://www.gstatic.com/firebasejs/8.2.0/firebase-messaging.js'); 4 | 5 | 6 | 7 | 8 | // Initialize the Firebase app in the service worker by passing the generated config 9 | var firebaseConfig = { 10 | apiKey: "AIzaSyD0dQIsSM2S-ToVv3NP1hqahEO3Nfkjbeo", 11 | authDomain: "reddit-guc.firebaseapp.com", 12 | projectId: "reddit-guc", 13 | storageBucket: "reddit-guc.appspot.com", 14 | messagingSenderId: "1072681859465", 15 | appId: "1:1072681859465:web:8898b5300e2a82c313c958", 16 | measurementId: "G-W3Y9L5DVM6" 17 | }; 18 | 19 | // eslint-disable-next-line no-undef 20 | firebase.initializeApp(firebaseConfig); 21 | 22 | // Retrieve firebase messaging 23 | // eslint-disable-next-line no-undef 24 | const messaging = firebase.messaging(); 25 | 26 | messaging.onBackgroundMessage(function(payload) { 27 | console.log('Received background message ', payload); 28 | 29 | const notificationTitle = payload.notification.title; 30 | const notificationOptions = { 31 | body: payload.notification.body, 32 | }; 33 | 34 | self.registration.showNotification(notificationTitle, 35 | notificationOptions); 36 | }); -------------------------------------------------------------------------------- /apps/user_app/src/main/java/org/sab/user/commands/ViewAnotherProfile.java: -------------------------------------------------------------------------------- 1 | package org.sab.user.commands; 2 | 3 | import org.sab.models.user.User; 4 | import org.sab.models.user.UserAttributes; 5 | import org.sab.service.Responder; 6 | import org.sab.service.validation.HTTPMethod; 7 | import org.sab.validation.Schema; 8 | import org.sab.validation.exceptions.EnvironmentVariableNotLoaded; 9 | 10 | import java.sql.SQLException; 11 | 12 | public class ViewAnotherProfile extends UserCommand { 13 | @Override 14 | protected String execute() { 15 | if (!uriParams.has(USERNAME)) 16 | return Responder.makeErrorResponse("You must add username in URIParams!", 400); 17 | String username = uriParams.getString(USERNAME); 18 | 19 | try { 20 | User user = getUser(username, UserAttributes.USERNAME, UserAttributes.PHOTO_URL); 21 | return Responder.makeDataResponse(user.toJSON()); 22 | } catch (SQLException | EnvironmentVariableNotLoaded e) { 23 | return Responder.makeErrorResponse(e.getMessage(), 400); 24 | } 25 | 26 | } 27 | 28 | @Override 29 | protected Schema getSchema() { 30 | return Schema.emptySchema(); 31 | } 32 | 33 | @Override 34 | protected HTTPMethod getMethodType() { 35 | return HTTPMethod.GET; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reddit-cli", 3 | "version": "0.2.0", 4 | "license": "MIT", 5 | "bin": "cli.js", 6 | "engines": { 7 | "node": ">=10" 8 | }, 9 | "scripts": { 10 | "start": "node cli.js", 11 | "test": "xo && ava" 12 | }, 13 | "files": [ 14 | "cli.js", 15 | "ui.js" 16 | ], 17 | "dependencies": { 18 | "import-jsx": "^4.0.0", 19 | "ink": "^3.0.8", 20 | "ink-big-text": "^1.2.0", 21 | "ink-gradient": "^2.0.0", 22 | "ink-select-input": "^4.2.0", 23 | "ink-spinner": "^4.0.1", 24 | "meow": "^9.0.0", 25 | "moment": "^2.29.1", 26 | "react": "^17.0.2", 27 | "websocket": "^1.0.34" 28 | }, 29 | "devDependencies": { 30 | "@ava/babel": "^1.0.1", 31 | "@babel/preset-env": "^7.14.1", 32 | "@babel/preset-react": "^7.13.13", 33 | "@babel/register": "^7.13.16", 34 | "ava": "^3.15.0", 35 | "chalk": "^4.1.1", 36 | "eslint-config-xo-react": "^0.25.0", 37 | "eslint-plugin-react": "^7.23.2", 38 | "eslint-plugin-react-hooks": "^4.2.0", 39 | "ink-testing-library": "^2.1.0", 40 | "xo": "^0.39.1" 41 | }, 42 | "ava": { 43 | "babel": true, 44 | "require": [ 45 | "@babel/register" 46 | ] 47 | }, 48 | "babel": { 49 | "presets": [ 50 | "@babel/preset-env", 51 | "@babel/preset-react" 52 | ] 53 | }, 54 | "xo": { 55 | "extends": "xo-react", 56 | "rules": { 57 | "react/prop-types": "off" 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /libs/utilities/src/main/java/org/sab/classes/ClassRegistry.java: -------------------------------------------------------------------------------- 1 | package org.sab.classes; 2 | 3 | import java.util.Map; 4 | import java.util.concurrent.ConcurrentHashMap; 5 | 6 | public class ClassRegistry { 7 | private static final ClassRegistry instance = new ClassRegistry(); 8 | private final Map> classes = new ConcurrentHashMap<>(); 9 | 10 | private ClassRegistry() { 11 | } 12 | 13 | public static ClassRegistry getInstance() { 14 | return instance; 15 | } 16 | 17 | public Class getClass(String name) throws ClassNotFoundException { 18 | final Class clazz = classes.get(name); 19 | 20 | if (clazz == null) { 21 | throw new ClassNotFoundException("Class not found in registry."); 22 | } 23 | 24 | return clazz; 25 | } 26 | 27 | public Class addClassByName(String name) throws ClassNotFoundException { 28 | final Class clazz = ByteClassLoader.loadClassByName(name); 29 | classes.put(name, clazz); 30 | return clazz; 31 | } 32 | 33 | public Class addClassByBytes(String name, byte[] b) { 34 | final Class clazz = ByteClassLoader.loadClassFromBytes(name, b); 35 | classes.put(name, clazz); 36 | return clazz; 37 | } 38 | 39 | public void removeClass(String name) { 40 | classes.remove(name); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /apps/user_app/src/main/java/org/sab/user/commands/ViewMyProfile.java: -------------------------------------------------------------------------------- 1 | package org.sab.user.commands; 2 | 3 | import org.sab.models.user.User; 4 | import org.sab.models.user.UserAttributes; 5 | import org.sab.service.Responder; 6 | import org.sab.service.validation.HTTPMethod; 7 | import org.sab.validation.Schema; 8 | import org.sab.validation.exceptions.EnvironmentVariableNotLoaded; 9 | 10 | import java.sql.SQLException; 11 | 12 | public class ViewMyProfile extends UserCommand { 13 | @Override 14 | protected String execute() { 15 | String username = authenticationParams.getString(USERNAME); 16 | 17 | try { 18 | User user = getUser(username, UserAttributes.USERNAME, UserAttributes.PASSWORD, UserAttributes.EMAIL, UserAttributes.BIRTHDATE, UserAttributes.PHOTO_URL, UserAttributes.USER_ID); 19 | return Responder.makeDataResponse(user.toJSON()); 20 | } catch (SQLException | EnvironmentVariableNotLoaded e) { 21 | return Responder.makeErrorResponse(e.getMessage(), 500); 22 | } 23 | } 24 | 25 | @Override 26 | protected Schema getSchema() { 27 | return Schema.emptySchema(); 28 | } 29 | 30 | @Override 31 | protected HTTPMethod getMethodType() { 32 | return HTTPMethod.GET; 33 | } 34 | 35 | @Override 36 | protected boolean isAuthNeeded() { 37 | return true; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /libs/utilities/src/main/java/org/sab/classes/Reader.java: -------------------------------------------------------------------------------- 1 | package org.sab.classes; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.net.URISyntaxException; 6 | import java.net.URL; 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | import java.nio.file.Paths; 10 | 11 | public class Reader { 12 | private Reader() { 13 | } 14 | 15 | public static byte[] getBytes(Path path) throws IOException { 16 | byte[] buffer; 17 | 18 | try (final InputStream inputStream = Files.newInputStream(path)) { 19 | buffer = inputStream.readAllBytes(); 20 | } 21 | 22 | return buffer; 23 | } 24 | 25 | public static Path getResourcePath(String resource) { 26 | final URL url = Reader.class.getClassLoader().getResource(resource); 27 | 28 | if (url == null) { 29 | return null; 30 | } 31 | 32 | try { 33 | return Paths.get(url.toURI()); 34 | } catch (URISyntaxException e) { 35 | return null; 36 | } 37 | } 38 | 39 | public static byte[] readBytesFromResource(String resource) throws IOException { 40 | final Path path = getResourcePath(resource); 41 | if(path == null) { 42 | throw new IOException("Path could not be found."); 43 | } 44 | 45 | return Reader.getBytes(path); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /apps/chat_app/chat_server/src/main/java/org/sab/chat/server/routers/AddMemberRouter.java: -------------------------------------------------------------------------------- 1 | package org.sab.chat.server.routers; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import org.json.simple.JSONObject; 5 | import org.sab.chat.server.ClientManager; 6 | 7 | import java.util.UUID; 8 | 9 | public class AddMemberRouter extends Router { 10 | 11 | @Override 12 | public void forwardRequestToQueue(ChannelHandlerContext ctx, JSONObject request) { 13 | boolean isAuthenticated = authenticate(request, "adminId"); 14 | if (!isAuthenticated) { 15 | rejectUnAuthenticatedRequest(ctx); 16 | return; 17 | } 18 | String[] attributes = {"chatId", "adminId", "memberId"}; 19 | packAndForwardRequest(ctx, request, attributes); 20 | } 21 | 22 | @Override 23 | public void routeResponse(ChannelHandlerContext ctx, JSONObject response) { 24 | JSONObject data = (JSONObject) response.get("data"); 25 | if(data == null) { 26 | handleError(ctx, response); 27 | return; 28 | } 29 | UUID chatId = UUID.fromString((String) data.get("chatId")); 30 | UUID targetMemberId = UUID.fromString((String) data.get("targetMemberId")); 31 | ClientManager.handleMemberAdded(chatId, targetMemberId); 32 | 33 | ClientManager.broadcastResponseToChatChannels(chatId, response); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cli/app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const React = require('react') 3 | const importJsx = require('import-jsx') 4 | const ChatContext = require('./contexts/chat-context') 5 | const { useState } = require('react') 6 | const { getAuthToken, mapSpecialIdToId } = require('./utils/id-mapper') 7 | const WelcomeFlow = importJsx('./flows/welcome-flow') 8 | const ChatFlow = importJsx('./flows/chat-flow') 9 | 10 | const AppContext = importJsx('./contexts/app-context') 11 | 12 | const defaultChatContext = { 13 | isLoadingChats: true, 14 | isLoadingMessages: true, 15 | highlightedChat: null, 16 | directChats: [], 17 | groupChats: [], 18 | selectedChat: null, 19 | messages: {}, 20 | sendToChat: null 21 | } 22 | 23 | const App = ({ command = 'welcome', flags }) => { 24 | const [chatContext, setChatContext] = useState(defaultChatContext) 25 | 26 | const { rainbow, user = '6b57562b-3667-426c-ae6a-372c8ea6ff91' } = flags 27 | 28 | const defaultAppContext = { 29 | authToken: getAuthToken(user), 30 | userId: mapSpecialIdToId(user) 31 | } 32 | 33 | return ( 34 | 35 | {command.toLowerCase() === 'welcome' && } 36 | 37 | {command.toLowerCase() === 'chat' && } 38 | 39 | 40 | ) 41 | } 42 | 43 | module.exports = App 44 | -------------------------------------------------------------------------------- /cli/components/create-group-chat.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const { Text, Box, useInput } = require('ink') 3 | const { useContext } = require('react') 4 | const ChatContext = require('../contexts/chat-context') 5 | const importJsx = require('import-jsx') 6 | const AppContext = require('../contexts/app-context') 7 | const TextInput = importJsx('./text-input') 8 | 9 | const { useState } = React 10 | 11 | const CreateGroupChat = ({ onBack }) => { 12 | const { authToken } = useContext(AppContext) 13 | const [chatContext, _] = useContext(ChatContext) 14 | const [chatName, setChatName] = useState('') 15 | 16 | const onCreate = (chatName) => { 17 | chatContext.sendToChat({ 18 | type: 'CREATE_GROUP_CHAT', 19 | authToken, 20 | name: chatName, 21 | description: '' 22 | }) 23 | onBack() 24 | } 25 | 26 | useInput((input, _) => { 27 | if (input.charCodeAt(0) === 60) { 28 | onBack() 29 | } 30 | }) 31 | 32 | return ( 33 | 34 | Create a Group Chat 35 | 36 | 37 | {'>'} 38 | 39 | 45 | 46 | {`\nPress “<” to go back`} 47 | 48 | ) 49 | } 50 | 51 | module.exports = CreateGroupChat 52 | -------------------------------------------------------------------------------- /apps/notification_app/src/test/java/org/sab/notification/commands/SendNotificationTest.java: -------------------------------------------------------------------------------- 1 | package org.sab.notification.commands; 2 | 3 | import org.json.JSONObject; 4 | import org.junit.Ignore; 5 | import org.junit.Test; 6 | import org.sab.service.validation.HTTPMethod; 7 | import org.sab.tests.TestsUtils; 8 | 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | import static org.junit.Assert.assertTrue; 13 | 14 | @Ignore 15 | public class SendNotificationTest { 16 | 17 | private static final String USERNAME = "scale-a-bull"; 18 | 19 | @Test 20 | public void shouldSendSingularWithSuccess() { 21 | final JSONObject body = new JSONObject( 22 | Map.of( 23 | "usersList", List.of(USERNAME), 24 | "title", "Some Title", 25 | "notificationBody", "Lorem ipsum" 26 | ) 27 | ); 28 | JSONObject request = TestsUtils.makeAuthorizedRequest(body, HTTPMethod.PUT.toString(), new JSONObject()); 29 | final JSONObject response = new JSONObject(new SendNotification().execute(request)); 30 | String notificationResult = response.getJSONObject("data").getString("notificationResult"); 31 | final boolean isSuccessful = notificationResult.startsWith("Successfully sent message"); 32 | assertTrue("Should send successfully", isSuccessful); 33 | } 34 | 35 | 36 | } 37 | -------------------------------------------------------------------------------- /cli/flows/chat-flow.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const React = require('react') 3 | const importJsx = require('import-jsx') 4 | const ChatContext = require('../contexts/chat-context') 5 | const { useEffect, useContext } = require('react') 6 | const useChatService = require('../hooks/chat-service') 7 | const LoadingSpinner = importJsx('../components/loading-spinner') 8 | const ChatsList = importJsx('../components/chats-list') 9 | const ChatView = importJsx('../components/chat-view') 10 | 11 | const ChatFlow = () => { 12 | const [chatContext, setChatContext] = useContext(ChatContext) 13 | 14 | const sendToChat = useChatService() 15 | 16 | useEffect(() => setChatContext({ ...chatContext, sendToChat }), []) 17 | 18 | const onChatSelect = (chat) => { 19 | setChatContext({ 20 | ...chatContext, 21 | selectedChat: chat, 22 | isLoadingMessages: true 23 | }) 24 | } 25 | const onChatExit = () => { 26 | setChatContext({ ...chatContext, selectedChat: null }) 27 | } 28 | 29 | return ( 30 | 31 | {!chatContext.isLoadingChats ? ( 32 | 33 | {chatContext.selectedChat ? ( 34 | 35 | ) : ( 36 | 37 | )} 38 | 39 | ) : ( 40 | 41 | )} 42 | 43 | ) 44 | } 45 | 46 | module.exports = ChatFlow 47 | -------------------------------------------------------------------------------- /apps/chat_app/chat_server/src/main/java/org/sab/chat/server/routers/RemoveMemberRouter.java: -------------------------------------------------------------------------------- 1 | package org.sab.chat.server.routers; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import org.json.simple.JSONObject; 5 | import org.sab.chat.server.ClientManager; 6 | 7 | import java.util.UUID; 8 | 9 | public class RemoveMemberRouter extends Router { 10 | 11 | @Override 12 | public void forwardRequestToQueue(ChannelHandlerContext ctx, JSONObject request) { 13 | boolean isAuthenticated = authenticate(request, "adminId"); 14 | if (!isAuthenticated) { 15 | rejectUnAuthenticatedRequest(ctx); 16 | return; 17 | } 18 | String[] attributes = {"chatId", "adminId", "memberId"}; 19 | packAndForwardRequest(ctx, request, attributes); 20 | } 21 | 22 | @Override 23 | public void routeResponse(ChannelHandlerContext ctx, JSONObject response) { 24 | JSONObject data = (JSONObject) response.get("data"); 25 | if(data == null) { 26 | handleError(ctx, response); 27 | return; 28 | } 29 | UUID chatId = UUID.fromString((String) data.get("chatId")); 30 | UUID targetMemberId = UUID.fromString((String) data.get("targetMemberId")); 31 | 32 | ClientManager.broadcastResponseToChatChannels(chatId, response); 33 | 34 | ClientManager.handleMemberRemoved(chatId, targetMemberId); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /apps/example_app/src/main/java/org/sab/demo/commands/HelloArango.java: -------------------------------------------------------------------------------- 1 | package org.sab.demo.commands; 2 | 3 | import com.arangodb.entity.BaseDocument; 4 | import org.sab.arango.Arango; 5 | import org.sab.service.validation.CommandWithVerification; 6 | import org.sab.service.validation.HTTPMethod; 7 | import org.sab.validation.Schema; 8 | 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | 13 | public class HelloArango extends CommandWithVerification { 14 | 15 | @Override 16 | protected String execute() { 17 | // Return SUCCESS 18 | String DB_NAME = System.getenv("ARANGO_DB"); 19 | String COLLECTION_NAME = "Hello"; 20 | 21 | Arango arango = Arango.getInstance(); 22 | 23 | arango.createCollectionIfNotExists( DB_NAME, COLLECTION_NAME, false); 24 | 25 | Map properties = new HashMap<>(); 26 | properties.put("hello", true); 27 | properties.put("bye", "not bye"); 28 | BaseDocument helloDoc = new BaseDocument(properties); 29 | 30 | arango.createDocument(DB_NAME, COLLECTION_NAME, helloDoc); 31 | 32 | return "{\"msg\":\"Successfully inserted Hello\", \"statusCode\": 200}"; 33 | } 34 | 35 | @Override 36 | protected Schema getSchema() { 37 | return Schema.emptySchema(); 38 | } 39 | 40 | @Override 41 | protected HTTPMethod getMethodType() { 42 | return HTTPMethod.POST; 43 | } 44 | } -------------------------------------------------------------------------------- /libs/models/src/main/java/org/sab/models/CollectionNames.java: -------------------------------------------------------------------------------- 1 | package org.sab.models; 2 | 3 | public enum CollectionNames { 4 | 5 | // model collections 6 | THREAD("Thread"), 7 | SUBTHREAD("Subthread"), 8 | COMMENT("Comment"), 9 | USER("User"), 10 | // # Subthread collections 11 | USER_CREATE_SUBTHREAD("UserCreateSubthread"), 12 | USER_BOOKMARK_SUBTHREAD("UserBookmarkSubthread"), 13 | USER_LIKE_SUBTHREAD("UserLikeSubthread"), 14 | USER_DISLIKE_SUBTHREAD("UserDislikeSubthread"), 15 | SUBTHREAD_REPORTS("SubthreadReports"), 16 | USER_FOLLOW_USER("UserFollowUser"), 17 | USER_MOD_THREAD("UserModThread"), 18 | // Comments collections 19 | USER_CREATE_COMMENT("UserCreateComment"), 20 | CONTENT_COMMENT("ContentComment"), 21 | USER_LIKE_COMMENT("UserLikeComment"), 22 | USER_DISLIKE_COMMENT("UserDislikeComment"), 23 | // Thread Collection 24 | USER_FOLLOW_THREAD("UserFollowThread"), 25 | USER_BOOKMARK_THREAD("UserBookmarkThread"), 26 | THREAD_CONTAIN_SUBTHREAD("ThreadContainSubThread"), 27 | USER_BANNED_FROM_THREAD("UserBannedFromThread"), 28 | USER_BLOCK_USER("UserBlockUser"); 29 | 30 | 31 | private final String collectionName; 32 | 33 | CollectionNames(String collectionName) { 34 | this.collectionName = collectionName; 35 | } 36 | 37 | public String get() { 38 | return this.collectionName; 39 | } 40 | 41 | } 42 | 43 | -------------------------------------------------------------------------------- /apps/user_app/src/main/java/org/sab/user/UserApp.java: -------------------------------------------------------------------------------- 1 | package org.sab.user; 2 | 3 | 4 | import org.sab.arango.Arango; 5 | import org.sab.functions.Utilities; 6 | import org.sab.models.CollectionNames; 7 | import org.sab.postgres.PostgresConnection; 8 | import org.sab.service.Service; 9 | import org.sab.validation.exceptions.EnvironmentVariableNotLoaded; 10 | 11 | import java.io.IOException; 12 | 13 | public class UserApp extends Service { 14 | public static final String ARANGO_DB_NAME = System.getenv("ARANGO_DB"); 15 | 16 | public static void main(String[] args) throws IOException, EnvironmentVariableNotLoaded { 17 | 18 | new UserApp().start(); 19 | dbInit(); 20 | } 21 | 22 | public static void dbInit() throws IOException, EnvironmentVariableNotLoaded { 23 | if (!Utilities.inContainerizationMode()) 24 | PostgresConnection.dbInit(); 25 | arangoDbInit(); 26 | } 27 | 28 | private static void arangoDbInit() throws EnvironmentVariableNotLoaded { 29 | if (ARANGO_DB_NAME == null) 30 | throw new EnvironmentVariableNotLoaded("ARANGO_DB"); 31 | Arango arango = Arango.getInstance(); 32 | arango.createDatabaseIfNotExists(ARANGO_DB_NAME); 33 | arango.createCollectionIfNotExists(ARANGO_DB_NAME, CollectionNames.USER.get(), false); 34 | } 35 | 36 | @Override 37 | public String getAppUriName() { 38 | return "user"; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/example_app/src/test/java/org/sab/demo/CommandNeedingAuthTest.java: -------------------------------------------------------------------------------- 1 | package org.sab.demo; 2 | 3 | import org.json.JSONObject; 4 | import org.junit.Test; 5 | import org.sab.auth.AuthParamsHandler; 6 | import org.sab.demo.commands.CommandNeedingAuth; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | import static org.junit.Assert.assertTrue; 10 | 11 | public class CommandNeedingAuthTest { 12 | 13 | @Test 14 | public void willFailDueToUnAuthentication() { 15 | JSONObject request = new JSONObject().put("uriParams", new JSONObject()).put("methodType", "GET"); 16 | AuthParamsHandler.putUnauthorizedParams(request); 17 | JSONObject response = new JSONObject(new CommandNeedingAuth().execute(request)); 18 | int statusCode = response.getInt("statusCode"); 19 | assertTrue(statusCode == 401 || statusCode == 403); 20 | assertTrue(response.getString("msg").startsWith("Unauthorized")); 21 | } 22 | 23 | @Test 24 | public void willSucceedAfterAuthentication() { 25 | JSONObject request = new JSONObject().put("uriParams", new JSONObject()).put("methodType", "GET"); 26 | AuthParamsHandler.putAuthorizedParams(request); 27 | JSONObject response = new JSONObject(new CommandNeedingAuth().execute(request)); 28 | assertEquals(200, response.getInt("statusCode")); 29 | assertEquals("Authentication successful!", response.getString("msg")); 30 | } 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /cli/components/create-direct-chat.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const { Text, Box, useInput } = require('ink') 3 | const { useContext } = require('react') 4 | const ChatContext = require('../contexts/chat-context') 5 | const { mapSpecialIdToId } = require('../utils/id-mapper') 6 | const importJsx = require('import-jsx') 7 | const AppContext = require('../contexts/app-context') 8 | const TextInput = importJsx('./text-input') 9 | 10 | const { useState } = React 11 | 12 | const CreateDirectChat = ({ onBack }) => { 13 | const { authToken } = useContext(AppContext) 14 | const [chatContext, _] = useContext(ChatContext) 15 | const [otherUserId, setOtherUserId] = useState('') 16 | 17 | const onCreate = (targetUserId) => { 18 | chatContext.sendToChat({ 19 | type: 'CREATE_DIRECT_CHAT', 20 | authToken, 21 | secondMember: mapSpecialIdToId(targetUserId) 22 | }) 23 | onBack() 24 | } 25 | 26 | useInput((input, _) => { 27 | if (input.charCodeAt(0) === 60) { 28 | onBack() 29 | } 30 | }) 31 | 32 | return ( 33 | 34 | Create a DM 35 | 36 | 37 | {'>'} 38 | 39 | 45 | 46 | {`\nPress “<” to go back`} 47 | 48 | ) 49 | } 50 | 51 | module.exports = CreateDirectChat 52 | -------------------------------------------------------------------------------- /apps/chat_app/chat_server/src/main/java/org/sab/chat/server/routers/CreateGroupChatRouter.java: -------------------------------------------------------------------------------- 1 | package org.sab.chat.server.routers; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import org.json.simple.JSONObject; 5 | import org.sab.chat.server.ClientManager; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.UUID; 10 | 11 | public class CreateGroupChatRouter extends Router { 12 | 13 | @Override 14 | public void forwardRequestToQueue(ChannelHandlerContext ctx, JSONObject request) { 15 | boolean isAuthenticated = authenticate(request, "creator"); 16 | if (!isAuthenticated) { 17 | rejectUnAuthenticatedRequest(ctx); 18 | return; 19 | } 20 | String[] attributes = {"creator", "name", "description"}; 21 | packAndForwardRequest(ctx, request, attributes); 22 | } 23 | 24 | @Override 25 | public void routeResponse(ChannelHandlerContext ctx, JSONObject response) { 26 | JSONObject data = (JSONObject) response.get("data"); 27 | if(data == null) { 28 | handleError(ctx, response); 29 | return; 30 | } 31 | 32 | UUID chatId = UUID.fromString((String) data.get("chatId")); 33 | 34 | List memberIds = new ArrayList<>(); 35 | memberIds.add(UUID.fromString((String) data.get("adminId"))); 36 | 37 | ClientManager.handleUserCreateChat(chatId, memberIds); 38 | 39 | ClientManager.broadcastResponseToChatChannels(chatId, response); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /apps/chat_app/chat_server/src/main/java/org/sab/chat/server/routers/LeaveGroupRouter.java: -------------------------------------------------------------------------------- 1 | package org.sab.chat.server.routers; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import org.json.simple.JSONObject; 5 | import org.sab.chat.server.ClientManager; 6 | 7 | import java.util.UUID; 8 | 9 | public class LeaveGroupRouter extends Router { 10 | 11 | @Override 12 | public void forwardRequestToQueue(ChannelHandlerContext ctx, JSONObject request) { 13 | boolean isAuthenticated = authenticate(request, "userId"); 14 | if (!isAuthenticated) { 15 | rejectUnAuthenticatedRequest(ctx); 16 | return; 17 | } 18 | String[] attributes = {"chatId", "userId"}; 19 | packAndForwardRequest(ctx, request, attributes); 20 | } 21 | 22 | @Override 23 | public void routeResponse(ChannelHandlerContext ctx, JSONObject response) { 24 | JSONObject data = (JSONObject) response.get("data"); 25 | if(data == null) { 26 | handleError(ctx, response); 27 | return; 28 | } 29 | UUID chatId = UUID.fromString((String) data.get("chatId")); 30 | UUID targetMemberId = UUID.fromString((String) data.get("targetMemberId")); 31 | UUID adminId = UUID.fromString((String) data.get("adminId")); 32 | 33 | ClientManager.broadcastResponseToChatChannels(chatId, response); 34 | 35 | boolean isAdmin = targetMemberId.equals(adminId); 36 | ClientManager.handleUserLeftGroup(chatId, targetMemberId, isAdmin); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /cli/components/add-group-member.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const { Text, Box, useInput } = require('ink') 3 | const { useContext } = require('react') 4 | const ChatContext = require('../contexts/chat-context') 5 | const importJsx = require('import-jsx') 6 | const AppContext = require('../contexts/app-context') 7 | const { mapSpecialIdToId } = require('../utils/id-mapper') 8 | const TextInput = importJsx('./text-input') 9 | 10 | const { useState } = React 11 | 12 | const AddGroupMember = ({ onBack }) => { 13 | const { authToken } = useContext(AppContext) 14 | const [chatContext, _] = useContext(ChatContext) 15 | const [memberId, setMemberId] = useState('') 16 | 17 | const chat = chatContext.highlightedChat 18 | 19 | const onAction = (memberId) => { 20 | chatContext.sendToChat({ 21 | authToken, 22 | type: 'ADD_GROUP_MEMBER', 23 | chatId: chat.chatId, 24 | memberId: mapSpecialIdToId(memberId) 25 | }) 26 | onBack() 27 | } 28 | 29 | useInput((input, _) => { 30 | if (input.charCodeAt(0) === 60) { 31 | onBack() 32 | } 33 | }) 34 | 35 | return ( 36 | 37 | {`Add a new member to ${chat.name}`} 38 | 39 | 40 | {'>'} 41 | 42 | 48 | 49 | {`\nPress “<” to go back`} 50 | 51 | ) 52 | } 53 | 54 | module.exports = AddGroupMember 55 | -------------------------------------------------------------------------------- /apps/chat_app/chat_storage/src/test/java/org/sab/chat/storage/config/CassandraConnectorTest.java: -------------------------------------------------------------------------------- 1 | package org.sab.chat.storage.config; 2 | 3 | import com.datastax.driver.core.ResultSet; 4 | import org.junit.After; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.sab.chat.storage.config.CassandraConnector; 8 | import org.sab.databases.PoolDoesNotExistException; 9 | 10 | import java.util.List; 11 | import java.util.stream.Collectors; 12 | 13 | import static org.junit.Assert.assertEquals; 14 | 15 | public class CassandraConnectorTest { 16 | private CassandraConnector cassandra; 17 | 18 | @Before 19 | public void connect() { 20 | cassandra = CassandraConnector.getConnectedInstance(); 21 | } 22 | 23 | @After 24 | public void disconnect() throws PoolDoesNotExistException { 25 | cassandra.destroyPool(); 26 | } 27 | 28 | @Test 29 | public void whenCreatingKeyspace_thenCreated() { 30 | String keyspaceName = cassandra.getKeyspaceName(); 31 | 32 | ResultSet result = 33 | cassandra.runQuery("SELECT * FROM system_schema.keyspaces;"); 34 | 35 | List matchedKeyspaces = result.all() 36 | .stream() 37 | .filter(row -> row.getString(0).equals(keyspaceName.toLowerCase())) 38 | .map(row -> row.getString(0)) 39 | .collect(Collectors.toList()); 40 | 41 | assertEquals(1, matchedKeyspaces.size()); 42 | assertEquals(matchedKeyspaces.get(0), keyspaceName.toLowerCase()); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /apps/subthread_app/src/main/resources/commandmap.properties: -------------------------------------------------------------------------------- 1 | ADD_IMAGE_TO_SUBTHREAD=org.sab.subthread.commands.AddImageToSubThread 2 | BOOKMARK_SUBTHREAD=org.sab.subthread.commands.BookmarkSubThread 3 | CREATE_COMMENT=org.sab.subthread.commands.CreateComment 4 | CREATE_SUBTHREAD=org.sab.subthread.commands.CreateSubThread 5 | DELETE_COMMENT=org.sab.subthread.commands.DeleteComment 6 | DELETE_SUBTHREAD=org.sab.subthread.commands.DeleteSubThread 7 | DISLIKE_COMMENT=org.sab.subthread.commands.DislikeComment 8 | DISLIKE_SUBTHREAD=org.sab.subthread.commands.DislikeSubThread 9 | GET_COMMENT=org.sab.subthread.commands.GetComment 10 | GET_COMMENTS=org.sab.subthread.commands.GetComments 11 | GET_MY_COMMENTS=org.sab.subthread.commands.GetMyComments 12 | GET_MY_DISLIKED_COMMENTS=org.sab.subthread.commands.GetMyDislikedComments 13 | GET_MY_DISLIKED_SUBTHREADS=org.sab.subthread.commands.GetMyDislikedSubThreads 14 | GET_MY_LIKED_COMMENTS=org.sab.subthread.commands.GetMyLikedComments 15 | GET_MY_LIKED_SUBTHREADS=org.sab.subthread.commands.GetMyLikedSubThreads 16 | GET_MY_SUBTHREADS=org.sab.subthread.commands.GetMySubThreads 17 | GET_SUBTHREAD=org.sab.subthread.commands.GetSubThread 18 | GET_SUBTHREADS=org.sab.subthread.commands.GetSubThreads 19 | LIKE_COMMENT=org.sab.subthread.commands.LikeComment 20 | LIKE_SUBTHREAD=org.sab.subthread.commands.LikeSubThread 21 | MODERATOR_SEE_REPORTS=org.sab.subthread.commands.ModeratorSeeReports 22 | REPORT_SUBTHREAD=org.sab.subthread.commands.ReportSubThread 23 | UPDATE_COMMENT=org.sab.subthread.commands.UpdateComment 24 | UPDATE_SUBTHREAD=org.sab.subthread.commands.UpdateSubThread 25 | -------------------------------------------------------------------------------- /apps/recommendation_app/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | rabbitmq: 4 | image: "rabbitmq:management" 5 | ports: 6 | - 15672:15672 7 | arangodb: 8 | image: "arangodb" 9 | ports: 10 | - 8529:8529 11 | volumes: 12 | - arangodb_data:/var/lib/arangodb3 13 | - arangodb_apps_data:/var/lib/arangodb3-apps 14 | environment: 15 | - ARANGO_ROOT_PASSWORD=root 16 | couchbase: 17 | build: 18 | context: "../../" 19 | dockerfile: "libs/couchbase/Dockerfile" 20 | environment: 21 | - COUCHBASE_ADMINISTRATOR_USERNAME=Administrator 22 | - COUCHBASE_ADMINISTRATOR_PASSWORD=password 23 | - COUCHBASE_CLUSTER_NAME=Reddit 24 | - COUCHBASE_RAM=1024 25 | ports: 26 | - 8091-8094:8091-8094 27 | - 11210:11210 28 | volumes: 29 | - cb_data:/opt/couchbase/var 30 | recommendation_app: 31 | build: 32 | context: "../../" 33 | dockerfile: "apps/recommendation_app/Dockerfile" 34 | restart: on-failure 35 | ports: 36 | - "4004:4004" 37 | environment: 38 | - RABBIT_HOST=rabbitmq 39 | - ARANGO_DB=Reddit 40 | - ARANGO_USER=root 41 | - ARANGO_PASSWORD=root 42 | - ARANGO_HOST=arangodb 43 | - COUCHBASE_HOST=couchbase 44 | - COUCHBASE_USERNAME=Administrator 45 | - COUCHBASE_PASSWORD=password 46 | depends_on: 47 | - "rabbitmq" 48 | - "arangodb" 49 | - "couchbase" 50 | 51 | networks: 52 | default: 53 | name: recommendation_nw 54 | 55 | volumes: 56 | cb_data: 57 | arangodb_data: 58 | arangodb_apps_data: 59 | -------------------------------------------------------------------------------- /apps/notification_app/src/main/java/org/sab/notification/commands/GetNotifications.java: -------------------------------------------------------------------------------- 1 | package org.sab.notification.commands; 2 | 3 | import com.google.cloud.firestore.CollectionReference; 4 | import org.json.JSONArray; 5 | import org.sab.models.user.UserAttributes; 6 | import org.sab.notification.FirestoreConnector; 7 | import org.sab.notification.NotificationApp; 8 | import org.sab.service.Responder; 9 | import org.sab.service.validation.CommandWithVerification; 10 | import org.sab.service.validation.HTTPMethod; 11 | import org.sab.validation.Schema; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.concurrent.ExecutionException; 17 | 18 | public class GetNotifications extends CommandWithVerification { 19 | 20 | @Override 21 | protected String execute() { 22 | String username = authenticationParams.getString(UserAttributes.USERNAME.toString()); 23 | FirestoreConnector firestoreConnector = FirestoreConnector.getInstance(); 24 | 25 | List> notifications = firestoreConnector.readCollection(NotificationApp.getNotificationsCollectionName(username)); 26 | 27 | return Responder.makeDataResponse(new JSONArray(notifications)).toString(); 28 | } 29 | 30 | @Override 31 | protected Schema getSchema() { 32 | return Schema.emptySchema(); 33 | } 34 | 35 | @Override 36 | protected HTTPMethod getMethodType() { 37 | return HTTPMethod.GET; 38 | } 39 | 40 | @Override 41 | protected boolean isAuthNeeded() { 42 | return true; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /apps/chat_app/chat_server/src/main/java/org/sab/chat/server/routers/CreateDirectChatRouter.java: -------------------------------------------------------------------------------- 1 | package org.sab.chat.server.routers; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import org.json.simple.JSONObject; 5 | import org.sab.chat.server.ClientManager; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.UUID; 10 | 11 | public class CreateDirectChatRouter extends Router { 12 | 13 | @Override 14 | public void forwardRequestToQueue(ChannelHandlerContext ctx, JSONObject request) { 15 | boolean isAuthenticated = authenticate(request, "firstMember"); 16 | if (!isAuthenticated) { 17 | rejectUnAuthenticatedRequest(ctx); 18 | return; 19 | } 20 | String[] attributes = {"firstMember", "secondMember"}; 21 | packAndForwardRequest(ctx, request, attributes); 22 | } 23 | 24 | @Override 25 | public void routeResponse(ChannelHandlerContext ctx, JSONObject response) { 26 | JSONObject data = (JSONObject) response.get("data"); 27 | if(data == null) { 28 | handleError(ctx, response); 29 | return; 30 | } 31 | 32 | UUID chatId = UUID.fromString((String) data.get("chatId")); 33 | 34 | List memberIds = new ArrayList<>(); 35 | memberIds.add(UUID.fromString((String) data.get("firstMember"))); 36 | memberIds.add(UUID.fromString((String) data.get("secondMember"))); 37 | 38 | ClientManager.handleUserCreateChat(chatId, memberIds); 39 | 40 | ClientManager.broadcastResponseToChatChannels(chatId, response); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /cli/components/remove-group-member.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const { Text, Box, useInput } = require('ink') 3 | const { useContext } = require('react') 4 | const ChatContext = require('../contexts/chat-context') 5 | const importJsx = require('import-jsx') 6 | const AppContext = require('../contexts/app-context') 7 | const { mapSpecialIdToId } = require('../utils/id-mapper') 8 | const TextInput = importJsx('./text-input') 9 | 10 | const { useState } = React 11 | 12 | const RemoveGroupMember = ({ onBack }) => { 13 | const { authToken } = useContext(AppContext) 14 | const [chatContext, _] = useContext(ChatContext) 15 | const [memberId, setMemberId] = useState('') 16 | 17 | const chat = chatContext.highlightedChat 18 | 19 | const onAction = (memberId) => { 20 | chatContext.sendToChat({ 21 | type: 'REMOVE_GROUP_MEMBER', 22 | chatId: chat.chatId, 23 | authToken, 24 | memberId: mapSpecialIdToId(memberId) 25 | }) 26 | onBack() 27 | } 28 | 29 | useInput((input, _) => { 30 | if (input.charCodeAt(0) === 60) { 31 | onBack() 32 | } 33 | }) 34 | 35 | return ( 36 | 37 | {`Remove a group member from ${chat.name}`} 38 | 39 | 40 | {'>'} 41 | 42 | 48 | 49 | {`\nPress “<” to go back`} 50 | 51 | ) 52 | } 53 | 54 | module.exports = RemoveGroupMember 55 | -------------------------------------------------------------------------------- /apps/example_app/src/test/java/org/sab/demo/CommandNeedingClaimsTest.java: -------------------------------------------------------------------------------- 1 | package org.sab.demo; 2 | 3 | import org.json.JSONObject; 4 | import org.junit.Test; 5 | import org.sab.auth.AuthParamsHandler; 6 | import org.sab.demo.commands.CommandNeedingAuth; 7 | import org.sab.demo.commands.CommandNeedingClaims; 8 | 9 | import static org.junit.Assert.assertEquals; 10 | import static org.junit.Assert.assertTrue; 11 | 12 | public class CommandNeedingClaimsTest { 13 | 14 | @Test 15 | public void willFailDueToUnAuthentication() { 16 | JSONObject request = new JSONObject().put("uriParams", new JSONObject()).put("methodType", "GET"); 17 | AuthParamsHandler.putUnauthorizedParams(request); 18 | JSONObject response = new JSONObject(new CommandNeedingAuth().execute(request)); 19 | int statusCode = response.getInt("statusCode"); 20 | assertTrue(statusCode == 401 || statusCode == 403); 21 | assertTrue(response.getString("msg").startsWith("Unauthorized")); 22 | } 23 | 24 | @Test 25 | public void willSucceedAfterAuthentication() { 26 | JSONObject request = new JSONObject().put("uriParams", new JSONObject()).put("methodType", "GET"); 27 | String username = "scale-a-bull"; 28 | JSONObject claims = new JSONObject().put("username", username); 29 | AuthParamsHandler.putAuthorizedParams(request, claims); 30 | JSONObject response = new JSONObject(new CommandNeedingClaims().execute(request)); 31 | assertEquals(200, response.getInt("statusCode")); 32 | assertEquals(String.format("Hello %s!", username), response.getString("msg")); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /apps/user_app/src/main/java/org/sab/user/commands/Login.java: -------------------------------------------------------------------------------- 1 | package org.sab.user.commands; 2 | 3 | import org.json.JSONObject; 4 | import org.sab.auth.Jwt; 5 | import org.sab.service.validation.HTTPMethod; 6 | import org.sab.validation.Attribute; 7 | import org.sab.validation.DataType; 8 | import org.sab.validation.Schema; 9 | 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | public class Login extends UserCommand { 14 | 15 | 16 | protected Schema getSchema() { 17 | Attribute username = new Attribute(USERNAME, DataType.USERNAME, true); 18 | Attribute password = new Attribute(PASSWORD, DataType.PASSWORD, true); 19 | return new Schema(List.of(username, password)); 20 | } 21 | 22 | @Override 23 | protected HTTPMethod getMethodType() { 24 | return HTTPMethod.POST; 25 | } 26 | 27 | @Override 28 | protected String execute() { 29 | String username = body.getString(USERNAME); 30 | String password = body.getString(PASSWORD); 31 | 32 | JSONObject userAuth = authenticateUser(username, password); 33 | if (userAuth.getInt("statusCode") != 200) 34 | return userAuth.toString(); 35 | 36 | Map claims = Map.of(USERNAME, username); 37 | String token = Jwt.generateToken(claims, 60); 38 | 39 | return makeJwtResponse(token); 40 | } 41 | 42 | private static String makeJwtResponse(String token) { 43 | JSONObject response = new JSONObject().put("msg", "Login Successful!").put("token", token); 44 | response.put("statusCode", 200); 45 | return response.toString(); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /apps/chat_app/chat_server/src/main/java/org/sab/chat/server/ChatServerInitializer.java: -------------------------------------------------------------------------------- 1 | package org.sab.chat.server; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.channel.ChannelInitializer; 5 | import io.netty.channel.ChannelPipeline; 6 | import io.netty.channel.group.ChannelGroup; 7 | import io.netty.handler.codec.http.HttpObjectAggregator; 8 | import io.netty.handler.codec.http.HttpServerCodec; 9 | import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; 10 | import io.netty.handler.stream.ChunkedWriteHandler; 11 | import org.sab.chat.server.handlers.HttpRequestHandler; 12 | import org.sab.chat.server.handlers.QueueHandler; 13 | import org.sab.chat.server.handlers.ResponseHandler; 14 | import org.sab.chat.server.handlers.TextWebSocketFrameHandler; 15 | 16 | public class ChatServerInitializer extends ChannelInitializer { 17 | private final ChannelGroup group; 18 | 19 | public ChatServerInitializer(ChannelGroup group) { 20 | this.group = group; 21 | } 22 | 23 | @Override 24 | protected void initChannel(Channel ch) { 25 | ChannelPipeline pipeline = ch.pipeline(); 26 | pipeline.addLast(new HttpServerCodec()); 27 | pipeline.addLast(new ChunkedWriteHandler()); 28 | pipeline.addLast(new HttpObjectAggregator(64 * 1024)); 29 | pipeline.addLast(new HttpRequestHandler("/ws")); 30 | pipeline.addLast(new WebSocketServerProtocolHandler("/ws")); 31 | pipeline.addLast(new TextWebSocketFrameHandler(group)); 32 | pipeline.addLast(ChatServer.queueExecutorGroup, new QueueHandler("CHAT")); 33 | pipeline.addLast(new ResponseHandler()); 34 | } 35 | } -------------------------------------------------------------------------------- /libs/couchbase/src/main/java/org/sab/couchbase/config/buckets.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Listings", 4 | "ramQuotaMB": 100, 5 | "bucketType": "ephemeral", 6 | "authType": "sasl", 7 | "saslPassword": "", 8 | "evictionPolicy": "nruEviction", 9 | "replicaNumber": 0, 10 | "flushEnabled": 1 11 | }, 12 | { 13 | "name": "RecommendedSubThreads", 14 | "ramQuotaMB": 100, 15 | "bucketType": "ephemeral", 16 | "authType": "sasl", 17 | "saslPassword": "", 18 | "evictionPolicy": "nruEviction", 19 | "replicaNumber": 0, 20 | "flushEnabled": 1 21 | }, 22 | { 23 | "name": "RecommendedThreads", 24 | "ramQuotaMB": 100, 25 | "bucketType": "ephemeral", 26 | "authType": "sasl", 27 | "saslPassword": "", 28 | "evictionPolicy": "nruEviction", 29 | "replicaNumber": 0, 30 | "flushEnabled": 1 31 | }, 32 | { 33 | "name": "RecommendedUsers", 34 | "ramQuotaMB": 100, 35 | "bucketType": "ephemeral", 36 | "authType": "sasl", 37 | "saslPassword": "", 38 | "evictionPolicy": "nruEviction", 39 | "replicaNumber": 0, 40 | "flushEnabled": 1 41 | }, 42 | { 43 | "name": "Comments", 44 | "ramQuotaMB": 100, 45 | "bucketType": "ephemeral", 46 | "authType": "sasl", 47 | "saslPassword": "", 48 | "evictionPolicy": "nruEviction", 49 | "replicaNumber": 0, 50 | "flushEnabled": 1 51 | }, 52 | { 53 | "name": "TestBucket", 54 | "ramQuotaMB": 100, 55 | "bucketType": "ephemeral", 56 | "authType": "sasl", 57 | "saslPassword": "", 58 | "evictionPolicy": "nruEviction", 59 | "replicaNumber": 0, 60 | "flushEnabled": 1 61 | } 62 | ] 63 | -------------------------------------------------------------------------------- /apps/controller/src/main/java/org/sab/controller/ControllerClient.java: -------------------------------------------------------------------------------- 1 | package org.sab.controller; 2 | 3 | import io.netty.bootstrap.Bootstrap; 4 | import io.netty.channel.ChannelFuture; 5 | import io.netty.channel.ChannelInitializer; 6 | import io.netty.channel.EventLoopGroup; 7 | import io.netty.channel.nio.NioEventLoopGroup; 8 | import io.netty.channel.socket.SocketChannel; 9 | import io.netty.channel.socket.nio.NioSocketChannel; 10 | 11 | import java.net.InetSocketAddress; 12 | 13 | 14 | public class ControllerClient { 15 | private final String ip; 16 | private final int port; 17 | private final String message; 18 | 19 | public ControllerClient(String ip, int port, String message) { 20 | this.ip = ip; 21 | this.port = port; 22 | this.message = message; 23 | } 24 | 25 | 26 | public void start() throws Exception { 27 | EventLoopGroup group = new NioEventLoopGroup(); 28 | try { 29 | Bootstrap b = new Bootstrap(); 30 | b.group(group) 31 | .channel(NioSocketChannel.class) 32 | .remoteAddress(new InetSocketAddress(ip, port)) 33 | .handler(new ChannelInitializer() { 34 | @Override 35 | public void initChannel(SocketChannel ch) { 36 | ch.pipeline().addLast(new ControllerClientHandler(message)); 37 | } 38 | }); 39 | 40 | ChannelFuture f = b.connect().sync(); 41 | f.channel().closeFuture().sync(); 42 | } finally { 43 | group.shutdownGracefully().sync(); 44 | } 45 | 46 | } 47 | } -------------------------------------------------------------------------------- /apps/user_app/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | rabbitmq: 4 | image: "rabbitmq:management" 5 | ports: 6 | - "15672:15672" 7 | - "5762:5762" 8 | arangodb: 9 | image: "arangodb" 10 | environment: 11 | - ARANGO_ROOT_PASSWORD=root 12 | ports: 13 | - "1234:8529" 14 | postgres: 15 | image: postgres 16 | restart: always 17 | environment: 18 | - POSTGRES_PASSWORD=root 19 | volumes: 20 | - ../../libs/postgres/src/main/resources/sql/CreateUsersTable.sql:/docker-entrypoint-initdb.d/1_CreateTables.sql 21 | - ../../libs/postgres/src/main/resources/sql/CreateUserProcedures.sql:/docker-entrypoint-initdb.d/2_CreateProcedures.sql 22 | pgpool: 23 | image: bitnami/pgpool:4 24 | ports: 25 | - 5432:5432 26 | environment: 27 | - PGPOOL_BACKEND_NODES=0:postgres:5432 28 | - PGPOOL_SR_CHECK_USER=postgres 29 | - PGPOOL_SR_CHECK_PASSWORD=root 30 | - PGPOOL_ENABLE_LDAP=no 31 | - PGPOOL_ENABLE_POOL_HBA=no 32 | - PGPOOL_POSTGRES_USERNAME=postgres 33 | - PGPOOL_POSTGRES_PASSWORD=root 34 | - PGPOOL_ADMIN_USERNAME=postgres 35 | - PGPOOL_ADMIN_PASSWORD=root 36 | healthcheck: 37 | test: ["CMD", "/opt/bitnami/scripts/pgpool/healthcheck.sh"] 38 | interval: 10s 39 | timeout: 5s 40 | retries: 5 41 | user_app: 42 | build: 43 | context: "../../" 44 | dockerfile: "apps/user_app/Dockerfile" 45 | restart: on-failure 46 | ports: 47 | - "4008:4008" 48 | env_file: 49 | - "../../.env" 50 | 51 | depends_on: 52 | - "rabbitmq" 53 | - "arangodb" 54 | - "postgres" 55 | 56 | networks: 57 | default: 58 | name: user_nw 59 | -------------------------------------------------------------------------------- /libs/utilities/src/main/java/org/sab/classes/ByteClassLoader.java: -------------------------------------------------------------------------------- 1 | package org.sab.classes; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | 7 | class ByteClassLoader extends ClassLoader { 8 | private static final String CLASS_EXT = ".class"; 9 | private static final char PACKAGE_SEPARATOR = '.'; 10 | 11 | private ByteClassLoader() { 12 | } 13 | 14 | 15 | public static Class loadClassFromBytes(String name, byte[] b) { 16 | // Convert the entire array of bytes (0 offset) into a Class 17 | return new ByteClassLoader().defineClass(name, b, 0, b.length); 18 | } 19 | 20 | public static Class loadClassByName(String name) throws ClassNotFoundException { 21 | try { 22 | final byte[] b = readClassFileAsBytes(name); 23 | return loadClassFromBytes(name, b); 24 | } catch (IOException e) { 25 | throw new ClassNotFoundException("Class not found.", e); 26 | } 27 | } 28 | 29 | private static byte[] readClassFileAsBytes(String className) throws IOException { 30 | byte[] buffer; 31 | 32 | try (final InputStream inputStream = getClassInputStream(className)) { 33 | if (inputStream == null) { 34 | throw new IOException("Class file not found."); 35 | } 36 | buffer = inputStream.readAllBytes(); 37 | } 38 | 39 | return buffer; 40 | } 41 | 42 | private static InputStream getClassInputStream(String className) { 43 | final String path = className.replace(PACKAGE_SEPARATOR, File.separatorChar) + CLASS_EXT; 44 | return ByteClassLoader.class.getClassLoader().getResourceAsStream(path); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /libs/service/src/main/java/org/sab/service/controllerbackdoor/BackdoorServerHandler.java: -------------------------------------------------------------------------------- 1 | package org.sab.service.controllerbackdoor; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.buffer.Unpooled; 5 | import io.netty.channel.ChannelFutureListener; 6 | import io.netty.channel.ChannelHandler; 7 | import io.netty.channel.ChannelHandlerContext; 8 | import io.netty.channel.ChannelInboundHandlerAdapter; 9 | import org.json.JSONObject; 10 | import org.sab.service.Service; 11 | import org.sab.service.managers.ControlManager; 12 | 13 | 14 | @ChannelHandler.Sharable 15 | public class BackdoorServerHandler extends ChannelInboundHandlerAdapter { 16 | private final ControlManager controlManager; 17 | 18 | public BackdoorServerHandler(ControlManager controlManager) { 19 | this.controlManager = controlManager; 20 | } 21 | 22 | private String toString(Object msg) { 23 | ByteBuf buf = (ByteBuf) msg; 24 | StringBuilder sb = new StringBuilder(); 25 | while (buf.isReadable()) { 26 | sb.append((char) buf.readByte()); 27 | } 28 | return sb.toString(); 29 | 30 | } 31 | 32 | @Override 33 | public void channelRead(ChannelHandlerContext ctx, Object msg) { // 34 | String controllerCmd = toString(msg); 35 | controlManager.handleControllerMessage(new JSONObject(controllerCmd)); 36 | } 37 | 38 | @Override 39 | public void channelReadComplete(ChannelHandlerContext ctx) { 40 | ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); 41 | } 42 | 43 | @Override 44 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 45 | cause.printStackTrace(); 46 | ctx.close(); 47 | } 48 | 49 | 50 | } 51 | -------------------------------------------------------------------------------- /apps/subthread_app/src/main/java/org/sab/subthread/commands/GetComment.java: -------------------------------------------------------------------------------- 1 | package org.sab.subthread.commands; 2 | 3 | import com.arangodb.entity.BaseDocument; 4 | import org.sab.arango.Arango; 5 | import org.sab.models.CouchbaseBuckets; 6 | import org.sab.service.Responder; 7 | import org.sab.service.validation.HTTPMethod; 8 | import org.sab.validation.Schema; 9 | 10 | import java.util.List; 11 | 12 | public class GetComment extends CommentCommand { 13 | @Override 14 | protected HTTPMethod getMethodType() { 15 | return HTTPMethod.GET; 16 | } 17 | 18 | @Override 19 | protected String execute() { 20 | 21 | Arango arango; 22 | 23 | BaseDocument commentDocument; 24 | 25 | try { 26 | final String commentId = uriParams.getString(COMMENT_ID); 27 | 28 | arango = Arango.getInstance(); 29 | 30 | arango.createCollectionIfNotExists(DB_Name, COMMENT_COLLECTION_NAME, false); 31 | 32 | 33 | if (commentExistsInCouchbase(commentId)) { 34 | commentDocument = getDocumentFromCouchbase(CouchbaseBuckets.COMMENTS.get(), commentId); 35 | } else if (existsInArango(COMMENT_COLLECTION_NAME, commentId)) { 36 | commentDocument = arango.readDocument(DB_Name, COMMENT_COLLECTION_NAME, commentId); 37 | } else { 38 | return Responder.makeErrorResponse(OBJECT_NOT_FOUND, 404); 39 | } 40 | } catch (Exception e) { 41 | return Responder.makeErrorResponse(e.getMessage(), 404); 42 | } 43 | 44 | return Responder.makeDataResponse(baseDocumentToJson(commentDocument)); 45 | } 46 | 47 | @Override 48 | protected Schema getSchema() { 49 | return new Schema(List.of()); 50 | } 51 | } -------------------------------------------------------------------------------- /libs/utilities/src/test/java/org/sab/futures/FutureUtilTest.java: -------------------------------------------------------------------------------- 1 | package org.sab.futures; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import java.util.concurrent.Callable; 6 | import java.util.concurrent.ExecutionException; 7 | import java.util.concurrent.ExecutorService; 8 | import java.util.concurrent.Executors; 9 | import java.util.concurrent.Future; 10 | 11 | import org.junit.Test; 12 | 13 | public class FutureUtilTest { 14 | 15 | private static String loopUtilDone(Future future) { 16 | while (!future.isDone()) { 17 | // Keep waiting 18 | } 19 | 20 | try { 21 | return future.get(); 22 | } catch (InterruptedException | ExecutionException e) { 23 | return null; 24 | } 25 | } 26 | 27 | 28 | @Test 29 | public void shouldWaitForFuture() { 30 | final int actionThreadCount = 1; 31 | final ExecutorService actionPool = Executors.newFixedThreadPool(actionThreadCount); 32 | 33 | final Callable sayHi = () -> { 34 | Thread.sleep(1000); 35 | return "lazy hi"; 36 | }; 37 | 38 | final Future sayHiFuture = actionPool.submit(sayHi); 39 | 40 | final int observerThreadCount = 2; 41 | final ExecutorService observerPool = Executors.newFixedThreadPool(observerThreadCount); 42 | 43 | final Future loopToObserve = observerPool.submit(() -> loopUtilDone(sayHiFuture)); 44 | 45 | final Future awaitToObserve = observerPool.submit(() -> 46 | FutureUtil.await(sayHiFuture, e -> null) 47 | ); 48 | 49 | final String expected = loopUtilDone(loopToObserve); 50 | final String actual = loopUtilDone(awaitToObserve); 51 | 52 | assertEquals(expected, actual); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /notification-client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /libs/netty/src/main/java/org/sab/netty/middleware/QueueHandler.java: -------------------------------------------------------------------------------- 1 | package org.sab.netty.middleware; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.buffer.Unpooled; 5 | import io.netty.channel.ChannelHandlerContext; 6 | import io.netty.channel.SimpleChannelInboundHandler; 7 | import io.netty.util.CharsetUtil; 8 | import org.sab.netty.Server; 9 | import org.sab.rabbitmq.RPCClient; 10 | import org.sab.rabbitmq.SingleClientChannel; 11 | 12 | import java.io.IOException; 13 | import java.util.concurrent.TimeoutException; 14 | 15 | public class QueueHandler extends SimpleChannelInboundHandler { 16 | 17 | @Override 18 | protected void channelRead0(ChannelHandlerContext ctx, Object object) { 19 | ByteBuf buffer = (ByteBuf) object; 20 | String request = buffer.toString(CharsetUtil.UTF_8); 21 | 22 | String queueName = ctx.channel().attr(Server.QUEUE_KEY).get(); 23 | 24 | queueName = queueName.toUpperCase(); 25 | 26 | String reqQueueName = queueName + "_REQ"; 27 | String resQueueName = queueName + "_RES"; 28 | 29 | try (SingleClientChannel channelExecutor = RPCClient.getSingleChannelExecutor()) { 30 | String response = channelExecutor.call(request, reqQueueName, resQueueName); 31 | ByteBuf content = Unpooled.copiedBuffer(response, CharsetUtil.UTF_8); 32 | ctx.fireChannelRead(content.copy()); 33 | } catch (IOException | TimeoutException | InterruptedException | NullPointerException e) { 34 | e.printStackTrace(); 35 | } 36 | } 37 | 38 | @Override 39 | public void channelReadComplete(ChannelHandlerContext ctx) { 40 | ctx.flush(); 41 | ctx.fireChannelReadComplete(); 42 | } 43 | 44 | @Override 45 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 46 | cause.printStackTrace(); 47 | ctx.close(); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /apps/chat_app/chat_storage/src/main/java/org/sab/chat/storage/models/DirectChat.java: -------------------------------------------------------------------------------- 1 | package org.sab.chat.storage.models; 2 | 3 | import com.datastax.driver.mapping.annotations.PartitionKey; 4 | import com.datastax.driver.mapping.annotations.Table; 5 | import org.json.simple.JSONObject; 6 | 7 | import java.util.UUID; 8 | 9 | @Table(keyspace = "chat_app", name = "direct_chats") 10 | public class DirectChat { 11 | 12 | @PartitionKey 13 | private UUID chat_id; 14 | private UUID first_member; 15 | private UUID second_member; 16 | 17 | public DirectChat() { 18 | } 19 | 20 | public DirectChat(UUID chat_id, UUID first_member, UUID second_member) { 21 | this.chat_id = chat_id; 22 | this.first_member = first_member; 23 | this.second_member = second_member; 24 | } 25 | 26 | public UUID getChat_id() { 27 | return chat_id; 28 | } 29 | 30 | public void setChat_id(UUID chat_id) { 31 | this.chat_id = chat_id; 32 | } 33 | 34 | public UUID getFirst_member() { 35 | return first_member; 36 | } 37 | 38 | public void setFirst_member(UUID first_member) { 39 | this.first_member = first_member; 40 | } 41 | 42 | public UUID getSecond_member() { 43 | return second_member; 44 | } 45 | 46 | public void setSecond_member(UUID second_member) { 47 | this.second_member = second_member; 48 | } 49 | 50 | public JSONObject toJson() { 51 | JSONObject json = new JSONObject(); 52 | json.put("chatId", chat_id.toString()); 53 | json.put("firstMember", first_member.toString()); 54 | json.put("secondMember", second_member.toString()); 55 | return json; 56 | } 57 | 58 | @Override 59 | public String toString() { 60 | return "DirectChat{" + 61 | "chat_id=" + chat_id + 62 | ", first_member=" + first_member + 63 | ", second_member=" + second_member + 64 | '}'; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /apps/chat_app/chat_server/src/main/java/org/sab/chat/server/handlers/TextWebSocketFrameHandler.java: -------------------------------------------------------------------------------- 1 | package org.sab.chat.server.handlers; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import io.netty.channel.SimpleChannelInboundHandler; 5 | import io.netty.channel.group.ChannelGroup; 6 | import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; 7 | import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; 8 | import org.json.simple.parser.JSONParser; 9 | import org.json.simple.parser.ParseException; 10 | import org.sab.chat.server.ClientManager; 11 | import org.json.simple.JSONObject; 12 | 13 | import java.util.UUID; 14 | 15 | public class TextWebSocketFrameHandler extends 16 | SimpleChannelInboundHandler { 17 | private final ChannelGroup group; 18 | 19 | public TextWebSocketFrameHandler(ChannelGroup group) { 20 | this.group = group; 21 | } 22 | 23 | @Override 24 | public void userEventTriggered(ChannelHandlerContext ctx, 25 | Object evt) throws Exception { 26 | if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) { 27 | ctx.pipeline().remove(HttpRequestHandler.class); 28 | group.add(ctx.channel()); 29 | } else { 30 | super.userEventTriggered(ctx, evt); 31 | } 32 | } 33 | @Override 34 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 35 | UUID userId = ClientManager.getChannelUser(ctx.channel().id()); 36 | ClientManager.handleUserOffline(userId, ctx.channel()); 37 | super.channelInactive(ctx); 38 | } 39 | 40 | @Override 41 | public void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws ParseException { 42 | msg.retain(); 43 | JSONParser parser = new JSONParser(); 44 | JSONObject messageJson = (JSONObject) parser.parse(msg.text()); 45 | ClientManager.forwardRequestToQueue(messageJson, ctx); 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /apps/chat_app/chat_server/src/main/java/org/sab/chat/server/handlers/QueueHandler.java: -------------------------------------------------------------------------------- 1 | package org.sab.chat.server.handlers; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import io.netty.channel.SimpleChannelInboundHandler; 5 | import org.json.simple.JSONObject; 6 | import org.json.simple.parser.JSONParser; 7 | import org.json.simple.parser.ParseException; 8 | import org.sab.rabbitmq.RPCClient; 9 | import org.sab.rabbitmq.SingleClientChannel; 10 | 11 | import java.io.IOException; 12 | import java.util.concurrent.TimeoutException; 13 | 14 | public class QueueHandler extends SimpleChannelInboundHandler { 15 | private final String queueName; 16 | 17 | public QueueHandler(String queueName) { 18 | this.queueName = queueName; 19 | } 20 | 21 | @Override 22 | protected void channelRead0(ChannelHandlerContext ctx, JSONObject requestJson) { 23 | String request = requestJson.toString(); 24 | 25 | String reqQueueName = queueName + "_REQ"; 26 | String resQueueName = queueName + "_RES"; 27 | 28 | try (SingleClientChannel channelExecutor = RPCClient.getSingleChannelExecutor()) { 29 | String response = channelExecutor.call(request, reqQueueName, resQueueName); 30 | JSONParser parser = new JSONParser(); 31 | JSONObject responseJson = (JSONObject) parser.parse(response); 32 | responseJson.put("type", requestJson.get("functionName")); 33 | ctx.fireChannelRead(responseJson); 34 | } catch (IOException | TimeoutException | InterruptedException | NullPointerException | ParseException e) { 35 | e.printStackTrace(); 36 | } 37 | } 38 | 39 | @Override 40 | public void channelReadComplete(ChannelHandlerContext ctx) { 41 | ctx.flush(); 42 | ctx.fireChannelReadComplete(); 43 | } 44 | 45 | @Override 46 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 47 | cause.printStackTrace(); 48 | ctx.close(); 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /apps/recommendation_app/src/main/java/org/sab/recommendation/commands/GetPopularThreads.java: -------------------------------------------------------------------------------- 1 | package org.sab.recommendation.commands; 2 | 3 | import com.couchbase.client.core.error.CouchbaseException; 4 | import com.couchbase.client.core.error.DocumentNotFoundException; 5 | import com.couchbase.client.core.error.TimeoutException; 6 | import com.couchbase.client.java.json.JsonArray; 7 | import org.json.JSONArray; 8 | import org.json.JSONException; 9 | import org.sab.couchbase.Couchbase; 10 | import org.sab.service.Responder; 11 | import org.sab.service.validation.HTTPMethod; 12 | import org.sab.validation.Schema; 13 | 14 | public class GetPopularThreads extends RecommendationCommand { 15 | 16 | @Override 17 | public String execute() { 18 | try { 19 | Couchbase couchbase = Couchbase.getInstance(); 20 | couchbase.connectIfNotConnected(); 21 | 22 | JsonArray result = couchbase.getDocument(LISTINGS_BUCKET_NAME, LISTINGS_POPULAR_THREADS_KEY).getArray(THREADS_DATA_KEY); 23 | return Responder.makeDataResponse(new JSONArray(result.toString())).toString(); 24 | } catch (DocumentNotFoundException e) { 25 | return new UpdatePopularThreads().execute(); 26 | } catch (TimeoutException e) { 27 | return Responder.makeErrorResponse("Request to Couchbase timed out.", 408); 28 | } catch (CouchbaseException e) { 29 | return Responder.makeErrorResponse("Couchbase error: " + e.getMessage(), 500); 30 | } catch (JSONException e) { 31 | return Responder.makeErrorResponse("Failed to create data JSONArray from Couchbase results.", 500); 32 | } catch (Exception e) { 33 | return Responder.makeErrorResponse("Something went wrong: " + e.getMessage(), 500); 34 | } 35 | } 36 | 37 | @Override 38 | protected Schema getSchema() { 39 | return Schema.emptySchema(); 40 | } 41 | 42 | @Override 43 | protected HTTPMethod getMethodType() { 44 | return HTTPMethod.GET; 45 | } 46 | } -------------------------------------------------------------------------------- /apps/recommendation_app/src/main/java/org/sab/recommendation/commands/GetPopularSubThreads.java: -------------------------------------------------------------------------------- 1 | package org.sab.recommendation.commands; 2 | 3 | import com.couchbase.client.core.error.CouchbaseException; 4 | import com.couchbase.client.core.error.DocumentNotFoundException; 5 | import com.couchbase.client.core.error.TimeoutException; 6 | import com.couchbase.client.java.json.JsonArray; 7 | import org.json.JSONArray; 8 | import org.json.JSONException; 9 | import org.sab.couchbase.Couchbase; 10 | import org.sab.service.Responder; 11 | import org.sab.service.validation.HTTPMethod; 12 | import org.sab.validation.Schema; 13 | 14 | public class GetPopularSubThreads extends RecommendationCommand { 15 | 16 | @Override 17 | public String execute() { 18 | try { 19 | Couchbase couchbase = Couchbase.getInstance(); 20 | couchbase.connectIfNotConnected(); 21 | 22 | JsonArray result = couchbase.getDocument(LISTINGS_BUCKET_NAME, LISTINGS_POPULAR_SUB_THREADS_KEY).getArray(SUB_THREADS_DATA_KEY); 23 | return Responder.makeDataResponse(new JSONArray(result.toString())).toString(); 24 | } catch (DocumentNotFoundException e) { 25 | return new UpdatePopularSubThreads().execute(); 26 | } catch (TimeoutException e) { 27 | return Responder.makeErrorResponse("Request to Couchbase timed out.", 408); 28 | } catch (CouchbaseException e) { 29 | return Responder.makeErrorResponse("Couchbase error: " + e.getMessage(), 500); 30 | } catch (JSONException e) { 31 | return Responder.makeErrorResponse("Failed to create data JSONArray from Couchbase results.", 500); 32 | } catch (Exception e) { 33 | return Responder.makeErrorResponse("Something went wrong: " + e.getMessage(), 500); 34 | } 35 | } 36 | 37 | @Override 38 | protected Schema getSchema() { 39 | return Schema.emptySchema(); 40 | } 41 | 42 | @Override 43 | protected HTTPMethod getMethodType() { 44 | return HTTPMethod.GET; 45 | } 46 | } -------------------------------------------------------------------------------- /libs/service/src/main/java/org/sab/service/managers/QueueManager.java: -------------------------------------------------------------------------------- 1 | package org.sab.service.managers; 2 | 3 | import org.json.JSONObject; 4 | import org.sab.functions.TriFunction; 5 | import org.sab.rabbitmq.RPCServer; 6 | import org.sab.rabbitmq.SingleServerChannel; 7 | import org.sab.service.ServiceConstants; 8 | 9 | import java.io.IOException; 10 | import java.util.concurrent.TimeoutException; 11 | 12 | public class QueueManager { 13 | private SingleServerChannel singleServerChannel; 14 | 15 | private final InvocationManager invocationManager; 16 | private final String appUriName; 17 | 18 | public QueueManager(String appUriName, InvocationManager invocationManager) { 19 | this.appUriName = appUriName; 20 | this.invocationManager = invocationManager; 21 | } 22 | 23 | public void initAcceptingNewRequests() throws IOException, TimeoutException { 24 | initRPCServer(); 25 | } 26 | 27 | public void initRPCServer() throws IOException, TimeoutException { 28 | // initializing a connection with rabbitMQ and initializing the queue on which 29 | // the app listens 30 | final String queueName = appUriName.toUpperCase() + ServiceConstants.REQUEST_QUEUE_NAME_SUFFIX; 31 | 32 | TriFunction invokeCallback = invocationManager::invokeCommand; 33 | singleServerChannel = RPCServer.getSingleChannelExecutor(queueName, invokeCallback); 34 | } 35 | 36 | public void stopAcceptingNewRequests() throws IOException { 37 | singleServerChannel.pauseListening(); 38 | } 39 | 40 | public void startAcceptingNewRequests() throws IOException { 41 | singleServerChannel.startListening(); 42 | } 43 | 44 | public void dispose() { 45 | if(singleServerChannel == null) { 46 | return; 47 | } 48 | try { 49 | stopAcceptingNewRequests(); 50 | } catch (IOException e) { 51 | e.printStackTrace(); 52 | } 53 | singleServerChannel = null; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /libs/postgres/src/test/java/org/sab/postgres/PostgresConnectionTest.java: -------------------------------------------------------------------------------- 1 | package org.sab.postgres; 2 | 3 | import org.junit.Test; 4 | import org.sab.validation.exceptions.EnvironmentVariableNotLoaded; 5 | 6 | import java.sql.Connection; 7 | import java.sql.ResultSet; 8 | import java.sql.SQLException; 9 | import java.sql.Statement; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | public class PostgresConnectionTest { 14 | public void runDB() { 15 | try { 16 | PostgresConnection pg = PostgresConnection.getInstance(); 17 | Connection c = pg.connect(); 18 | Statement s = c.createStatement(); 19 | ResultSet rs = s.executeQuery("SELECT 1"); 20 | assertTrue(rs.next()); 21 | assertEquals(1, rs.getInt(1)); 22 | assertFalse(rs.next()); 23 | } catch (SQLException | EnvironmentVariableNotLoaded e) { 24 | fail(e.getMessage()); 25 | } 26 | } 27 | 28 | @Test 29 | public void postgresWorking() { 30 | runDB(); 31 | } 32 | 33 | @Test 34 | public void postgresIsSingleton() { 35 | PostgresConnection conn1 = null; 36 | PostgresConnection conn2 = null; 37 | try { 38 | conn1 = PostgresConnection.getInstance(); 39 | conn2 = PostgresConnection.getInstance(); 40 | } catch (EnvironmentVariableNotLoaded e) { 41 | fail(e.getMessage()); 42 | } 43 | assertSame(conn1, conn2); 44 | } 45 | 46 | @Test 47 | public void canCloseConnection() { 48 | PostgresConnection postgresConnection = null; 49 | try { 50 | postgresConnection = PostgresConnection.getInstance(); 51 | } catch (EnvironmentVariableNotLoaded e) { 52 | fail(e.getMessage()); 53 | } 54 | try { 55 | Connection connection = postgresConnection.connect(); 56 | connection.close(); 57 | } catch (SQLException e) { 58 | fail(e.getMessage()); 59 | } 60 | 61 | } 62 | 63 | 64 | } 65 | -------------------------------------------------------------------------------- /apps/subthread_app/src/main/java/org/sab/subthread/commands/GetSubThreads.java: -------------------------------------------------------------------------------- 1 | package org.sab.subthread.commands; 2 | 3 | import com.arangodb.ArangoCursor; 4 | import com.arangodb.entity.BaseDocument; 5 | import org.json.JSONArray; 6 | import org.sab.arango.Arango; 7 | import org.sab.service.Responder; 8 | import org.sab.service.validation.HTTPMethod; 9 | import org.sab.validation.Schema; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | public class GetSubThreads extends SubThreadCommand { 15 | 16 | @Override 17 | protected HTTPMethod getMethodType() { 18 | return HTTPMethod.GET; 19 | } 20 | 21 | @Override 22 | protected String execute() { 23 | Arango arango; 24 | JSONArray response; 25 | try { 26 | arango = Arango.getInstance(); 27 | 28 | // TODO not a uri param 29 | final String threadId = uriParams.getString(THREAD_ID); 30 | 31 | arango.createCollectionIfNotExists(DB_Name, THREAD_COLLECTION_NAME, false); 32 | arango.createCollectionIfNotExists(DB_Name, SUBTHREAD_COLLECTION_NAME, false); 33 | 34 | if (!arango.documentExists(DB_Name, THREAD_COLLECTION_NAME, threadId)) { 35 | return Responder.makeErrorResponse(OBJECT_NOT_FOUND, 404); 36 | } 37 | 38 | ArangoCursor cursor = arango.filterCollection(DB_Name, SUBTHREAD_COLLECTION_NAME, PARENT_THREAD_ID_DB, threadId); 39 | ArrayList arr = new ArrayList<>(); 40 | arr.add(PARENT_THREAD_ID_DB); 41 | arr.add(CREATOR_ID_DB); 42 | arr.add(TITLE_DB); 43 | arr.add(CONTENT_DB); 44 | arr.add(LIKES_DB); 45 | arr.add(DISLIKES_DB); 46 | response = arango.parseOutput(cursor, SUBTHREAD_ID_DB, arr); 47 | 48 | } catch (Exception e) { 49 | return Responder.makeErrorResponse(e.getMessage(), 404); 50 | } 51 | return Responder.makeDataResponse(response).toString(); 52 | } 53 | 54 | @Override 55 | protected Schema getSchema() { 56 | return new Schema(List.of()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /apps/user_app/src/main/java/org/sab/user/commands/UpdatePassword.java: -------------------------------------------------------------------------------- 1 | package org.sab.user.commands; 2 | 3 | import org.json.JSONObject; 4 | import org.sab.auth.Auth; 5 | import org.sab.postgres.PostgresConnection; 6 | import org.sab.service.Responder; 7 | import org.sab.service.validation.HTTPMethod; 8 | import org.sab.validation.Attribute; 9 | import org.sab.validation.DataType; 10 | import org.sab.validation.Schema; 11 | import org.sab.validation.exceptions.EnvironmentVariableNotLoaded; 12 | 13 | import java.sql.SQLException; 14 | import java.util.List; 15 | 16 | public class UpdatePassword extends UserCommand { 17 | 18 | 19 | protected Schema getSchema() { 20 | Attribute oldPassword = new Attribute(OLD_PASSWORD, DataType.PASSWORD, true); 21 | Attribute newPassword = new Attribute(NEW_PASSWORD, DataType.PASSWORD, true); 22 | return new Schema(List.of(oldPassword, newPassword)); 23 | } 24 | 25 | @Override 26 | protected HTTPMethod getMethodType() { 27 | return HTTPMethod.PUT; 28 | } 29 | 30 | @Override 31 | protected boolean isAuthNeeded() { 32 | return true; 33 | } 34 | 35 | @Override 36 | protected String execute() { 37 | 38 | String username = authenticationParams.getString(USERNAME); 39 | String oldPassword = body.getString(OLD_PASSWORD); 40 | String newPassword = body.getString(NEW_PASSWORD); 41 | 42 | if (oldPassword.equals(newPassword)) 43 | return Responder.makeErrorResponse("Your new password cannot match your last one.", 400); 44 | 45 | JSONObject userAuth = authenticateUser(username, oldPassword); 46 | if (userAuth.getInt("statusCode") != 200) 47 | return userAuth.toString(); 48 | 49 | try { 50 | newPassword = Auth.hash(newPassword); 51 | PostgresConnection.call("update_user_password", username, newPassword); 52 | } catch (EnvironmentVariableNotLoaded | SQLException e) { 53 | return Responder.makeErrorResponse(e.getMessage(), 404); 54 | } 55 | 56 | 57 | return Responder.makeMsgResponse("Account Updated Successfully!"); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /apps/user_app/src/main/java/org/sab/user/commands/DeleteProfilePhoto.java: -------------------------------------------------------------------------------- 1 | package org.sab.user.commands; 2 | 3 | import org.sab.minio.MinIO; 4 | import org.sab.models.user.User; 5 | import org.sab.models.user.UserAttributes; 6 | import org.sab.postgres.PostgresConnection; 7 | import org.sab.service.Responder; 8 | import org.sab.service.validation.HTTPMethod; 9 | import org.sab.validation.Schema; 10 | import org.sab.validation.exceptions.EnvironmentVariableNotLoaded; 11 | 12 | import java.sql.SQLException; 13 | import java.util.List; 14 | 15 | public class DeleteProfilePhoto extends UserCommand { 16 | @Override 17 | protected Schema getSchema() { 18 | return new Schema(List.of()); 19 | } 20 | 21 | @Override 22 | protected HTTPMethod getMethodType() { 23 | return HTTPMethod.DELETE; 24 | } 25 | 26 | @Override 27 | protected boolean isAuthNeeded() { 28 | return true; 29 | } 30 | 31 | @Override 32 | protected String execute() { 33 | String username = authenticationParams.getString(USERNAME); 34 | User user; 35 | 36 | try { 37 | user = getUser(username, UserAttributes.PHOTO_URL, UserAttributes.USER_ID); 38 | if (user.getPhotoUrl() == null) 39 | return Responder.makeMsgResponse("You don't have a profile picture, you can't delete your avatar!"); 40 | 41 | } catch (EnvironmentVariableNotLoaded | SQLException e) { 42 | return Responder.makeErrorResponse(e.getMessage(), 502); 43 | } 44 | 45 | try { 46 | if (!MinIO.deleteObject(BUCKETNAME, user.reformatUserId())) 47 | return Responder.makeErrorResponse("Error Occurred While Deleting Your Image!", 404); 48 | } catch (EnvironmentVariableNotLoaded e) { 49 | return Responder.makeErrorResponse(e.getMessage(), 400); 50 | } 51 | 52 | try { 53 | PostgresConnection.call("delete_profile_picture", username); 54 | } catch (EnvironmentVariableNotLoaded | SQLException e) { 55 | return Responder.makeErrorResponse(e.getMessage(), 404); 56 | } 57 | 58 | return Responder.makeMsgResponse("Profile Picture deleted successfully"); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /apps/chat_app/chat_server/src/main/java/org/sab/chat/server/ChatServer.java: -------------------------------------------------------------------------------- 1 | package org.sab.chat.server; 2 | 3 | import io.netty.bootstrap.ServerBootstrap; 4 | import io.netty.channel.*; 5 | import io.netty.channel.group.ChannelGroup; 6 | import io.netty.channel.group.DefaultChannelGroup; 7 | import io.netty.channel.nio.NioEventLoopGroup; 8 | import io.netty.channel.socket.nio.NioServerSocketChannel; 9 | import io.netty.util.concurrent.DefaultEventExecutorGroup; 10 | import io.netty.util.concurrent.EventExecutorGroup; 11 | import io.netty.util.concurrent.ImmediateEventExecutor; 12 | 13 | import java.net.InetSocketAddress; 14 | 15 | public class ChatServer { 16 | 17 | private final ChannelGroup channelGroup = 18 | new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE); 19 | private final EventLoopGroup group = new NioEventLoopGroup(); 20 | protected static final EventExecutorGroup queueExecutorGroup = new DefaultEventExecutorGroup(8); 21 | private Channel channel; 22 | 23 | public ChannelFuture start(InetSocketAddress address) { 24 | ServerBootstrap bootstrap = new ServerBootstrap(); 25 | bootstrap.group(group) 26 | .channel(NioServerSocketChannel.class) 27 | .childHandler(createInitializer(channelGroup)); 28 | ChannelFuture future = bootstrap.bind(address); 29 | future.syncUninterruptibly(); 30 | channel = future.channel(); 31 | return future; 32 | } 33 | 34 | protected ChannelInitializer createInitializer( 35 | ChannelGroup group) { 36 | return new ChatServerInitializer(group); 37 | } 38 | 39 | public void destroy() { 40 | if (channel != null) { 41 | channel.close(); 42 | } 43 | channelGroup.close(); 44 | queueExecutorGroup.shutdownGracefully(); 45 | group.shutdownGracefully(); 46 | } 47 | 48 | public static void main(String[] args) { 49 | int port = 5000; 50 | final ChatServer endpoint = new ChatServer(); 51 | ChannelFuture future = endpoint.start(new InetSocketAddress(port)); 52 | Runtime.getRuntime().addShutdownHook(new Thread(endpoint::destroy)); 53 | future.channel().closeFuture().syncUninterruptibly(); 54 | } 55 | } -------------------------------------------------------------------------------- /apps/thread_app/src/main/java/org/sab/thread/commands/GetFollowedThreads.java: -------------------------------------------------------------------------------- 1 | package org.sab.thread.commands; 2 | 3 | import com.arangodb.ArangoCursor; 4 | import com.arangodb.entity.BaseDocument; 5 | import org.json.JSONArray; 6 | import org.sab.arango.Arango; 7 | import org.sab.service.Responder; 8 | import org.sab.service.validation.HTTPMethod; 9 | import org.sab.validation.Schema; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | public class GetFollowedThreads extends ThreadCommand { 15 | @Override 16 | protected boolean isAuthNeeded() { 17 | return true; 18 | } 19 | 20 | @Override 21 | protected HTTPMethod getMethodType() { 22 | return HTTPMethod.GET; 23 | } 24 | 25 | @Override 26 | protected String execute() { 27 | Arango arango = null; 28 | JSONArray response; 29 | try { 30 | arango = Arango.getInstance(); 31 | 32 | String userId = authenticationParams.getString(ThreadCommand.USERNAME); 33 | 34 | arango.createCollectionIfNotExists(DB_Name, USER_COLLECTION_NAME, false); 35 | arango.createCollectionIfNotExists(DB_Name, USER_FOLLOW_THREAD_COLLECTION_NAME, true); 36 | 37 | if (!arango.documentExists(DB_Name, USER_COLLECTION_NAME, userId)) { 38 | return Responder.makeErrorResponse(OBJECT_NOT_FOUND, 404); 39 | } 40 | ArangoCursor cursor = arango.filterEdgeCollection(DB_Name, USER_FOLLOW_THREAD_COLLECTION_NAME, USER_COLLECTION_NAME + "/" + userId); 41 | ArrayList arr = new ArrayList<>(); 42 | arr.add(NUM_OF_FOLLOWERS_DB); 43 | arr.add(DESCRIPTION_DB); 44 | arr.add(CREATOR_ID_DB); 45 | arr.add(DATE_CREATED_DB); 46 | response = arango.parseOutput(cursor, THREAD_NAME, arr); 47 | 48 | } catch (Exception e) { 49 | return Responder.makeErrorResponse(e.getMessage(), 404); 50 | } finally { 51 | if (arango != null) { 52 | 53 | } 54 | } 55 | return Responder.makeDataResponse(response).toString(); 56 | } 57 | 58 | @Override 59 | protected Schema getSchema() { 60 | return new Schema(List.of()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /apps/subthread_app/src/main/java/org/sab/subthread/commands/GetMyComments.java: -------------------------------------------------------------------------------- 1 | package org.sab.subthread.commands; 2 | 3 | import com.arangodb.ArangoCursor; 4 | import com.arangodb.entity.BaseDocument; 5 | import org.json.JSONArray; 6 | import org.sab.arango.Arango; 7 | import org.sab.service.Responder; 8 | import org.sab.service.validation.HTTPMethod; 9 | import org.sab.validation.Schema; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | public class GetMyComments extends CommentCommand { 15 | @Override 16 | protected boolean isAuthNeeded() { 17 | return true; 18 | } 19 | 20 | @Override 21 | protected HTTPMethod getMethodType() { 22 | return HTTPMethod.GET; 23 | } 24 | 25 | @Override 26 | protected String execute() { 27 | Arango arango; 28 | JSONArray response; 29 | 30 | try { 31 | arango = Arango.getInstance(); 32 | 33 | String userId = authenticationParams.getString(CommentCommand.USERNAME); 34 | 35 | arango.createCollectionIfNotExists(DB_Name, USER_COLLECTION_NAME, false); 36 | arango.createCollectionIfNotExists(DB_Name, COMMENT_COLLECTION_NAME, false); 37 | 38 | 39 | if (!arango.documentExists(DB_Name, USER_COLLECTION_NAME, userId)) { 40 | return Responder.makeErrorResponse(OBJECT_NOT_FOUND, 404); 41 | } 42 | ArangoCursor cursor = arango.filterCollection(DB_Name, COMMENT_COLLECTION_NAME, CREATOR_ID_DB, userId); 43 | ArrayList arr = new ArrayList<>(); 44 | arr.add(PARENT_SUBTHREAD_ID_DB); 45 | arr.add(CREATOR_ID_DB); 46 | arr.add(CONTENT_DB); 47 | arr.add(PARENT_CONTENT_TYPE_DB); 48 | arr.add(LIKES_DB); 49 | arr.add(DISLIKES_DB); 50 | arr.add(DATE_CREATED_DB); 51 | response = arango.parseOutput(cursor, COMMENT_ID_DB, arr); 52 | 53 | } catch (Exception e) { 54 | return Responder.makeErrorResponse(e.getMessage(), 404); 55 | } 56 | return Responder.makeDataResponse(response).toString(); 57 | } 58 | 59 | @Override 60 | protected Schema getSchema() { 61 | return new Schema(List.of()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /libs/netty/src/main/java/org/sab/netty/ServerInitializer.java: -------------------------------------------------------------------------------- 1 | package org.sab.netty; 2 | 3 | import io.netty.channel.ChannelInitializer; 4 | import io.netty.channel.ChannelPipeline; 5 | import io.netty.channel.socket.SocketChannel; 6 | import io.netty.handler.codec.http.HttpMethod; 7 | import io.netty.handler.codec.http.HttpObjectAggregator; 8 | import io.netty.handler.codec.http.HttpServerCodec; 9 | import io.netty.handler.codec.http.HttpServerExpectContinueHandler; 10 | import io.netty.handler.codec.http.cors.CorsConfig; 11 | import io.netty.handler.codec.http.cors.CorsConfigBuilder; 12 | import io.netty.handler.codec.http.cors.CorsHandler; 13 | import io.netty.handler.ssl.SslContext; 14 | import org.sab.netty.middleware.QueueHandler; 15 | import org.sab.netty.middleware.RequestHandler; 16 | import org.sab.netty.middleware.ResponseHandler; 17 | 18 | public class ServerInitializer extends ChannelInitializer { 19 | 20 | private final SslContext sslCtx; 21 | private static final int MAX_CONTENT_LENGTH = 5 * (1 << 20); // 5MB 22 | 23 | public ServerInitializer(SslContext sslCtx) { 24 | this.sslCtx = sslCtx; 25 | } 26 | 27 | @Override 28 | public void initChannel(SocketChannel ch) { 29 | CorsConfig corsConfig = CorsConfigBuilder.forAnyOrigin() 30 | .allowedRequestHeaders("X-Requested-With", "Content-Type", "Content-Length", "Authorization", "Function-Name") 31 | .allowedRequestMethods( 32 | HttpMethod.GET, 33 | HttpMethod.POST, 34 | HttpMethod.PUT, 35 | HttpMethod.DELETE, 36 | HttpMethod.OPTIONS) 37 | .build(); 38 | ChannelPipeline p = ch.pipeline(); 39 | if (sslCtx != null) { 40 | p.addLast(sslCtx.newHandler(ch.alloc())); 41 | } 42 | p.addLast(new HttpServerCodec()); 43 | p.addLast(new HttpServerExpectContinueHandler()); 44 | p.addLast(new CorsHandler(corsConfig)); 45 | p.addLast(new HttpObjectAggregator(MAX_CONTENT_LENGTH)); 46 | p.addLast(new RequestHandler()); 47 | p.addLast(Server.queueExecutorGroup, "QueueHandler", new QueueHandler()); 48 | p.addLast(new ResponseHandler()); 49 | 50 | } 51 | } -------------------------------------------------------------------------------- /apps/subthread_app/src/main/java/org/sab/subthread/commands/GetMyDislikedComments.java: -------------------------------------------------------------------------------- 1 | package org.sab.subthread.commands; 2 | 3 | import com.arangodb.ArangoCursor; 4 | import com.arangodb.entity.BaseDocument; 5 | import org.json.JSONArray; 6 | import org.sab.arango.Arango; 7 | import org.sab.service.Responder; 8 | import org.sab.service.validation.HTTPMethod; 9 | import org.sab.validation.Schema; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | public class GetMyDislikedComments extends CommentCommand { 15 | @Override 16 | protected boolean isAuthNeeded() { 17 | return true; 18 | } 19 | 20 | @Override 21 | protected HTTPMethod getMethodType() { 22 | return HTTPMethod.GET; 23 | } 24 | 25 | @Override 26 | protected String execute() { 27 | Arango arango; 28 | JSONArray response; 29 | 30 | try { 31 | arango = Arango.getInstance(); 32 | 33 | String userId = authenticationParams.getString(CommentCommand.USERNAME); 34 | 35 | arango.createCollectionIfNotExists(DB_Name, USER_COLLECTION_NAME, false); 36 | arango.createCollectionIfNotExists(DB_Name, USER_DISLIKE_COMMENT_COLLECTION_NAME, true); 37 | 38 | if (!arango.documentExists(DB_Name, USER_COLLECTION_NAME, userId)) { 39 | return Responder.makeErrorResponse(OBJECT_NOT_FOUND, 404); 40 | } 41 | ArangoCursor cursor = arango.filterEdgeCollection(DB_Name, USER_DISLIKE_COMMENT_COLLECTION_NAME, USER_COLLECTION_NAME + "/" + userId); 42 | ArrayList arr = new ArrayList<>(); 43 | arr.add(PARENT_SUBTHREAD_ID_DB); 44 | arr.add(CREATOR_ID_DB); 45 | arr.add(CONTENT_DB); 46 | arr.add(PARENT_CONTENT_TYPE_DB); 47 | arr.add(LIKES_DB); 48 | arr.add(DISLIKES_DB); 49 | arr.add(DATE_CREATED_DB); 50 | response = arango.parseOutput(cursor, COMMENT_ID_DB, arr); 51 | 52 | } catch (Exception e) { 53 | return Responder.makeErrorResponse(e.getMessage(), 404); 54 | } 55 | return Responder.makeDataResponse(response).toString(); 56 | } 57 | 58 | @Override 59 | protected Schema getSchema() { 60 | return new Schema(List.of()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /apps/subthread_app/src/main/java/org/sab/subthread/commands/GetMySubThreads.java: -------------------------------------------------------------------------------- 1 | package org.sab.subthread.commands; 2 | 3 | import com.arangodb.ArangoCursor; 4 | import com.arangodb.entity.BaseDocument; 5 | import org.json.JSONArray; 6 | import org.sab.arango.Arango; 7 | import org.sab.service.Responder; 8 | import org.sab.service.validation.HTTPMethod; 9 | import org.sab.validation.Schema; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | public class GetMySubThreads extends SubThreadCommand { 15 | 16 | @Override 17 | protected boolean isAuthNeeded() { 18 | return true; 19 | } 20 | 21 | @Override 22 | protected HTTPMethod getMethodType() { 23 | return HTTPMethod.GET; 24 | } 25 | 26 | @Override 27 | protected String execute() { 28 | Arango arango; 29 | JSONArray response; 30 | try { 31 | arango = Arango.getInstance(); 32 | 33 | String userId = authenticationParams.getString(SubThreadCommand.USERNAME); 34 | 35 | arango.createCollectionIfNotExists(DB_Name, THREAD_COLLECTION_NAME, false); 36 | arango.createCollectionIfNotExists(DB_Name, USER_COLLECTION_NAME, false); 37 | arango.createCollectionIfNotExists(DB_Name, SUBTHREAD_COLLECTION_NAME, false); 38 | 39 | 40 | if (!arango.documentExists(DB_Name, USER_COLLECTION_NAME, userId)) { 41 | return Responder.makeErrorResponse(OBJECT_NOT_FOUND, 404); 42 | } 43 | 44 | ArangoCursor cursor = arango.filterCollection(DB_Name, SUBTHREAD_COLLECTION_NAME, CREATOR_ID_DB, userId); 45 | ArrayList arr = new ArrayList<>(); 46 | arr.add(PARENT_THREAD_ID_DB); 47 | arr.add(CREATOR_ID_DB); 48 | arr.add(TITLE_DB); 49 | arr.add(CONTENT_DB); 50 | arr.add(LIKES_DB); 51 | arr.add(DISLIKES_DB); 52 | response = arango.parseOutput(cursor, SUBTHREAD_ID_DB, arr); 53 | 54 | } catch (Exception e) { 55 | return Responder.makeErrorResponse(e.getMessage(), 404); 56 | } 57 | return Responder.makeDataResponse(response).toString(); 58 | } 59 | 60 | @Override 61 | protected Schema getSchema() { 62 | return new Schema(List.of()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /apps/subthread_app/src/main/java/org/sab/subthread/commands/GetMyLikedComments.java: -------------------------------------------------------------------------------- 1 | package org.sab.subthread.commands; 2 | 3 | import com.arangodb.ArangoCursor; 4 | import com.arangodb.entity.BaseDocument; 5 | import org.json.JSONArray; 6 | import org.sab.arango.Arango; 7 | import org.sab.service.Responder; 8 | import org.sab.service.validation.HTTPMethod; 9 | import org.sab.validation.Schema; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | public class GetMyLikedComments extends CommentCommand { 15 | @Override 16 | protected boolean isAuthNeeded() { 17 | return true; 18 | } 19 | 20 | @Override 21 | protected HTTPMethod getMethodType() { 22 | return HTTPMethod.GET; 23 | } 24 | 25 | @Override 26 | protected String execute() { 27 | Arango arango; 28 | JSONArray response; 29 | 30 | try { 31 | arango = Arango.getInstance(); 32 | 33 | String userId = authenticationParams.getString(CommentCommand.USERNAME); 34 | 35 | arango.createCollectionIfNotExists(DB_Name, USER_COLLECTION_NAME, false); 36 | arango.createCollectionIfNotExists(DB_Name, USER_LIKE_COMMENT_COLLECTION_NAME, true); 37 | 38 | 39 | if (!arango.documentExists(DB_Name, USER_COLLECTION_NAME, userId)) { 40 | return Responder.makeErrorResponse(OBJECT_NOT_FOUND, 404).toString(); 41 | } 42 | ArangoCursor cursor = arango.filterEdgeCollection(DB_Name, USER_LIKE_COMMENT_COLLECTION_NAME, USER_COLLECTION_NAME + "/" + userId); 43 | ArrayList arr = new ArrayList<>(); 44 | arr.add(PARENT_SUBTHREAD_ID_DB); 45 | arr.add(CREATOR_ID_DB); 46 | arr.add(CONTENT_DB); 47 | arr.add(PARENT_CONTENT_TYPE_DB); 48 | arr.add(LIKES_DB); 49 | arr.add(DISLIKES_DB); 50 | arr.add(DATE_CREATED_DB); 51 | response = arango.parseOutput(cursor, COMMENT_ID_DB, arr); 52 | 53 | } catch (Exception e) { 54 | return Responder.makeErrorResponse(e.getMessage(), 404); 55 | } 56 | return Responder.makeDataResponse(response).toString(); 57 | } 58 | 59 | @Override 60 | protected Schema getSchema() { 61 | return new Schema(List.of()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /apps/subthread_app/src/main/java/org/sab/subthread/commands/GetMyDislikedSubThreads.java: -------------------------------------------------------------------------------- 1 | package org.sab.subthread.commands; 2 | 3 | import com.arangodb.ArangoCursor; 4 | import com.arangodb.entity.BaseDocument; 5 | import org.json.JSONArray; 6 | import org.sab.arango.Arango; 7 | import org.sab.service.Responder; 8 | import org.sab.service.validation.HTTPMethod; 9 | import org.sab.validation.Schema; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | public class GetMyDislikedSubThreads extends SubThreadCommand { 15 | @Override 16 | protected boolean isAuthNeeded() { 17 | return true; 18 | } 19 | 20 | @Override 21 | protected HTTPMethod getMethodType() { 22 | return HTTPMethod.GET; 23 | } 24 | 25 | @Override 26 | protected String execute() { 27 | Arango arango; 28 | JSONArray response; 29 | try { 30 | arango = Arango.getInstance(); 31 | 32 | String userId = authenticationParams.getString(SubThreadCommand.USERNAME); 33 | 34 | arango.createCollectionIfNotExists(DB_Name, USER_COLLECTION_NAME, false); 35 | arango.createCollectionIfNotExists(DB_Name, USER_DISLIKE_SUBTHREAD_COLLECTION_NAME, true); 36 | 37 | if (!existsInArango(USER_COLLECTION_NAME, userId)) { 38 | return Responder.makeErrorResponse(OBJECT_NOT_FOUND, 404); 39 | } 40 | ArangoCursor cursor = arango.filterEdgeCollection(DB_Name, USER_DISLIKE_SUBTHREAD_COLLECTION_NAME, USER_COLLECTION_NAME + "/" + userId); 41 | ArrayList arr = new ArrayList<>(); 42 | arr.add(PARENT_THREAD_ID_DB); 43 | arr.add(CREATOR_ID_DB); 44 | arr.add(CONTENT_DB); 45 | arr.add(TITLE_DB); 46 | arr.add(HAS_IMAGE_DB); 47 | arr.add(LIKES_DB); 48 | arr.add(DISLIKES_DB); 49 | arr.add(DATE_CREATED_DB); 50 | response = arango.parseOutput(cursor, SUBTHREAD_ID_DB, arr); 51 | 52 | } catch (Exception e) { 53 | return Responder.makeErrorResponse(e.getMessage(), 404); 54 | } 55 | return Responder.makeDataResponse(response).toString(); 56 | } 57 | 58 | @Override 59 | protected Schema getSchema() { 60 | return new Schema(List.of()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /apps/user_app/src/main/java/org/sab/user/commands/UpdateProfilePhoto.java: -------------------------------------------------------------------------------- 1 | package org.sab.user.commands; 2 | 3 | import org.sab.minio.MinIO; 4 | import org.sab.models.user.User; 5 | import org.sab.models.user.UserAttributes; 6 | import org.sab.postgres.PostgresConnection; 7 | import org.sab.service.Responder; 8 | import org.sab.service.validation.HTTPMethod; 9 | import org.sab.validation.Schema; 10 | import org.sab.validation.exceptions.EnvironmentVariableNotLoaded; 11 | 12 | import java.sql.SQLException; 13 | 14 | public class UpdateProfilePhoto extends UserCommand { 15 | @Override 16 | protected Schema getSchema() { 17 | return Schema.emptySchema(); 18 | 19 | } 20 | 21 | @Override 22 | protected HTTPMethod getMethodType() { 23 | return HTTPMethod.PUT; 24 | } 25 | 26 | @Override 27 | protected boolean isAuthNeeded() { 28 | return true; 29 | } 30 | 31 | @Override 32 | protected String execute() { 33 | if (files.length() != 1) 34 | return Responder.makeErrorResponse("Only one profile image allowed per upload, Check Form-Data Files!", 400); 35 | 36 | String username = authenticationParams.getString(USERNAME); 37 | String photoUrl; 38 | User user; 39 | try { 40 | user = getUser(username, UserAttributes.USER_ID); 41 | } catch (EnvironmentVariableNotLoaded | SQLException e) { 42 | return Responder.makeErrorResponse(e.getMessage(), 502); 43 | } 44 | 45 | try { 46 | photoUrl = MinIO.uploadObject(BUCKETNAME, user.reformatUserId(), files.getJSONObject("image")); 47 | if (photoUrl.isEmpty()) 48 | return Responder.makeErrorResponse("Error Occurred While Uploading Your Image!", 404); 49 | } catch (EnvironmentVariableNotLoaded e) { 50 | return Responder.makeErrorResponse(e.getMessage(), 400); 51 | } 52 | 53 | 54 | try { 55 | PostgresConnection.call("update_profile_picture", username, photoUrl); 56 | } catch (EnvironmentVariableNotLoaded | SQLException e) { 57 | return Responder.makeErrorResponse(e.getMessage(), 404); 58 | } 59 | 60 | return Responder.makeMsgResponse(String.format("Profile Picture uploaded successfully. You can find at %s", photoUrl)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /apps/subthread_app/src/main/java/org/sab/subthread/commands/GetMyLikedSubThreads.java: -------------------------------------------------------------------------------- 1 | package org.sab.subthread.commands; 2 | 3 | import com.arangodb.ArangoCursor; 4 | import com.arangodb.entity.BaseDocument; 5 | import org.json.JSONArray; 6 | import org.sab.arango.Arango; 7 | import org.sab.service.Responder; 8 | import org.sab.service.validation.HTTPMethod; 9 | import org.sab.validation.Schema; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | public class GetMyLikedSubThreads extends SubThreadCommand { 15 | 16 | @Override 17 | protected boolean isAuthNeeded() { 18 | return true; 19 | } 20 | 21 | @Override 22 | protected HTTPMethod getMethodType() { 23 | return HTTPMethod.GET; 24 | } 25 | 26 | @Override 27 | protected String execute() { 28 | Arango arango; 29 | JSONArray response; 30 | try { 31 | arango = Arango.getInstance(); 32 | 33 | String userId = authenticationParams.getString(SubThreadCommand.USERNAME); 34 | 35 | arango.createCollectionIfNotExists(DB_Name, USER_COLLECTION_NAME, false); 36 | arango.createCollectionIfNotExists(DB_Name, USER_LIKE_SUBTHREAD_COLLECTION_NAME, true); 37 | 38 | 39 | if (!existsInArango(USER_COLLECTION_NAME, userId)) { 40 | return Responder.makeErrorResponse(OBJECT_NOT_FOUND, 404); 41 | } 42 | ArangoCursor cursor = arango.filterEdgeCollection(DB_Name, USER_LIKE_SUBTHREAD_COLLECTION_NAME, USER_COLLECTION_NAME + "/" + userId); 43 | ArrayList arr = new ArrayList<>(); 44 | arr.add(PARENT_THREAD_ID_DB); 45 | arr.add(CREATOR_ID_DB); 46 | arr.add(CONTENT_DB); 47 | arr.add(TITLE_DB); 48 | arr.add(HAS_IMAGE_DB); 49 | arr.add(LIKES_DB); 50 | arr.add(DISLIKES_DB); 51 | arr.add(DATE_CREATED_DB); 52 | response = arango.parseOutput(cursor, SUBTHREAD_ID_DB, arr); 53 | 54 | } catch (Exception e) { 55 | return Responder.makeErrorResponse(e.getMessage(), 404); 56 | } 57 | return Responder.makeDataResponse(response).toString(); 58 | } 59 | 60 | @Override 61 | protected Schema getSchema() { 62 | return new Schema(List.of()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /apps/subthread_app/src/main/java/org/sab/subthread/commands/ModeratorSeeReports.java: -------------------------------------------------------------------------------- 1 | package org.sab.subthread.commands; 2 | 3 | import com.arangodb.ArangoCursor; 4 | import com.arangodb.entity.BaseDocument; 5 | import org.json.JSONArray; 6 | import org.sab.arango.Arango; 7 | import org.sab.service.Responder; 8 | import org.sab.service.validation.HTTPMethod; 9 | import org.sab.validation.Schema; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | public class ModeratorSeeReports extends SubThreadCommand { 15 | 16 | @Override 17 | protected Schema getSchema() { 18 | return new Schema(List.of()); 19 | } 20 | 21 | @Override 22 | protected HTTPMethod getMethodType() { 23 | return HTTPMethod.GET; 24 | } 25 | 26 | @Override 27 | protected String execute() { 28 | Arango arango; 29 | 30 | JSONArray response; 31 | 32 | try { 33 | // TODO why is the thread id not the user id in the URI? 34 | String threadId = uriParams.getString(THREAD_ID); 35 | 36 | arango = Arango.getInstance(); 37 | 38 | arango.createCollectionIfNotExists(DB_Name, THREAD_COLLECTION_NAME, false); 39 | 40 | arango.createCollectionIfNotExists(DB_Name, USER_COLLECTION_NAME, false); 41 | 42 | arango.createCollectionIfNotExists(DB_Name, SUBTHREAD_REPORTS_COLLECTION_NAME, false); 43 | 44 | ArangoCursor cursor = arango.filterCollection(SubThreadCommand.DB_Name, SubThreadCommand.SUBTHREAD_REPORTS_COLLECTION_NAME, SubThreadCommand.THREAD_ID_DB, threadId); 45 | ArrayList reportAtt = new ArrayList<>(); 46 | reportAtt.add(SubThreadCommand.REPORTER_ID_DB); 47 | reportAtt.add(SubThreadCommand.TYPE_OF_REPORT_DB); 48 | reportAtt.add(SubThreadCommand.THREAD_ID_DB); 49 | reportAtt.add(SubThreadCommand.DATE_CREATED_DB); 50 | reportAtt.add(SubThreadCommand.REPORT_MSG_DB); 51 | reportAtt.add(SubThreadCommand.SUBTHREAD_ID_DB); 52 | response = arango.parseOutput(cursor, SubThreadCommand.REPORT_ID_DB, reportAtt); 53 | 54 | } catch (Exception e) { 55 | return Responder.makeErrorResponse(e.getMessage(), 404); 56 | } 57 | return Responder.makeDataResponse(response).toString(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /cli/components/chat-view.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const { useState, useEffect, useContext } = require('react') 3 | const { Text, Box, useInput } = require('ink') 4 | const AppContext = require('../contexts/app-context') 5 | 6 | const importJsx = require('import-jsx') 7 | const TextInput = importJsx('./text-input') 8 | const LoadingSpinner = importJsx('./loading-spinner') 9 | const MessagesList = importJsx('./messages-list') 10 | 11 | const ChatContext = require('../contexts/chat-context') 12 | const { mapIdToSpecialId } = require('../utils/id-mapper') 13 | 14 | const ChatView = ({ chat, onChatExit }) => { 15 | const { userId, authToken } = useContext(AppContext) 16 | const [chatContext, _] = useContext(ChatContext) 17 | const [newMessage, setNewMessage] = useState('') 18 | 19 | const onNewMessageSent = (messageText) => { 20 | if (messageText.length == 0) return 21 | const isAbleToSend = chatContext.sendToChat({ 22 | authToken, 23 | type: chat.name ? 'CREATE_GROUP_MESSAGE' : 'CREATE_DIRECT_MESSAGE', 24 | chatId: chat.chatId, 25 | content: messageText 26 | }) 27 | if (isAbleToSend) setNewMessage('') 28 | else setNewMessage(`Didn't connect to web socket`) 29 | } 30 | 31 | useEffect(() => { 32 | chatContext.sendToChat({ 33 | type: chat.name ? 'GET_GROUP_MESSAGES' : 'GET_DIRECT_MESSAGES', 34 | chatId: chat.chatId, 35 | authToken 36 | }) 37 | }, []) 38 | 39 | useInput((input, _) => { 40 | if (input.charCodeAt(0) === 60) { 41 | onChatExit() 42 | } 43 | }) 44 | 45 | return ( 46 | 47 | {!chatContext.isLoadingMessages ? ( 48 | 49 | 50 | 51 | 52 | {'>'} 53 | 54 | 66 | 67 | {`\nPress “<” to exit chat`} 68 | 69 | ) : ( 70 | 71 | )} 72 | 73 | ) 74 | } 75 | 76 | module.exports = ChatView 77 | --------------------------------------------------------------------------------