Skip to content

Commit e1733f1

Browse files
authored
Merge branch 'main' into n2-user-question-history
2 parents ff0377f + 2a52d5e commit e1733f1

File tree

107 files changed

+10066
-35
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

107 files changed

+10066
-35
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,8 @@ ehthumbs.db
4444
/user-service/user-service/.env
4545
/backend/user-service/.env
4646
/frontend/.env
47+
/addon/chat/Chatio/obj
48+
/addon/chat/Chatio/Properties/PublishProfiles
49+
/addon/chat/Chatio/Properties/ServiceDependencies/Chatiox - Web Deploy
50+
/addon/chat/Chatio/.vs/Chatio
51+
/addon/chat/Chatio/bin

README.md

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,36 @@
55
This project follows a microservices architecture with the following services:
66
1. **Frontend** - Port `3000`
77
2. **User Service** - Port `3001`
8-
3. **Question Service** - Port `3002`
8+
3. **Question Service** - Port `3002`
99
4. **Matching Service** - Port `3003`
1010
5. **Collaboration Service** - Port `3004`
1111
6. **MongoDB** - Port `27017` (Database)
1212
7. **Nginx API Gateway** - Port `80`
1313
8. **Redis** - Port `6379`
1414
9. **Zookeeper** - Port `2181`
1515
10. **Kafka** - Port `9092`, Port `29092`
16+
11. **Chat Service** (Located in addon/chat)
1617

1718
### Setting up the Project
19+
20+
### Running the Project with `start.sh`
21+
22+
A `start.sh` script is provided in the root directory to simplify setup. This script will start all services, check for any `.env.example` files, and create the required `.env` files if they don’t already exist.
23+
24+
To run the project, open a terminal in the `cs3219-ay2425s1-project-g25` directory and execute:
25+
26+
./start.sh
27+
28+
This script can be run on **Linux**, **macOS**, and **Windows** (with Git Bash or WSL). It will initialize all services and ensure the environment files are set up as needed.
29+
30+
Ensure the script has execute permissions. If needed, set them by running:
31+
32+
chmod +x start.sh
33+
34+
35+
This will initialize all services and ensure the environment files are set up as needed. Note that running `start.sh` requires Git Bash, WSL, or a similar terminal environment capable of executing Bash scripts.
36+
37+
### Manual Setup
1838
Copy and paste the .env.example files in each service. Rename them as .env files.
1939
Files to do this in:
2040
1. ./
@@ -23,6 +43,7 @@ Files to do this in:
2343
4. /backend/question-service
2444
5. /backend/matching-service
2545
6. /backend/collaboration-service
46+
2647
Then, run `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"` twice to generate
2748
your 2 JWT token secrets. For the first one, paste it into the JWT_ACCESS_TOKEN_SECRET variable of
2849
the .env files in question-service and user-service. Then, copy the second into the
@@ -31,10 +52,15 @@ JWT_REFRESH_TOKEN_SECRET of the .env file in user-service.
3152
Further note: The DB_CLOUD_URI .env variable in user-service doesn't need to be filled in. A local
3253
database will be created in the mongoDB service.
3354

34-
Consult the readme files in the service if there are further configurations needed.
55+
Consult the readme files in each service if there are further configurations needed.
56+
3557
### Running the Project
3658

37-
To run all services, execute the following command in the root directory:
59+
To run the main services, execute the following command in the root directory:
60+
61+
`docker-compose up --build`
62+
63+
To run the chat service, navigate to the addon/chat directory and run:
3864

3965
`docker-compose up --build`
4066

@@ -49,8 +75,9 @@ Once the containers are up:
4975
- Redis: [http://localhost:6379](http://localhost:6379)
5076
- Zookeeper: [http://localhost:2181](http://localhost:2181)
5177
- Kafka: [http://localhost:9092](http://localhost:9092)
78+
- Chat Service: Available through the main application
5279

53-
Note that even after docker says that everything is up and running, there is a risk that they aren't when you load the frontend. Wait for the frontend logs to show up in the docker logs.
80+
Note that even after docker says that everything is up and running, there is a risk that they aren't when you load the frontend. Wait for the frontend logs to show up in the docker logs.
5481
In this event, wait for about a minute before trying again. If that still doesn't work and there are network errors, try
5582
rebuilding the services by running `docker-compose up --build` again.
5683

@@ -64,4 +91,4 @@ rebuilding the services by running `docker-compose up --build` again.
6491

6592
### Nginx API Gateway
6693

67-
- Nginx runs on port `80` and acts as the API gateway for routing requests to the respective services.
94+
- Nginx runs on port `80` and acts as the API gateway for routing requests to the respective services.
262 KB
Binary file not shown.
533 KB
Binary file not shown.

addon/chat/Chatio/Chatio.csproj

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<UserSecretsId>ebee6fc2-2cd0-4bfd-8580-ad97b28e9c5c</UserSecretsId>
8+
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
13+
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.10" />
14+
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.6" />
15+
<PackageReference Include="MongoDB.Driver" Version="3.0.0" />
16+
<PackageReference Include="OpenAI" Version="2.0.0" />
17+
<PackageReference Include="StackExchange.Redis" Version="2.8.16" />
18+
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
19+
</ItemGroup>
20+
21+
</Project>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3+
<PropertyGroup>
4+
<ActiveDebugProfile>http</ActiveDebugProfile>
5+
<Controller_SelectedScaffolderID>MvcControllerEmptyScaffolder</Controller_SelectedScaffolderID>
6+
<Controller_SelectedScaffolderCategoryPath>root/Common/MVC/Controller</Controller_SelectedScaffolderCategoryPath>
7+
<NameOfLastUsedPublishProfile>C:\Users\Wwj\source\repos\project_ai\Chatio\Properties\PublishProfiles\Chatiox - Web Deploy.pubxml</NameOfLastUsedPublishProfile>
8+
</PropertyGroup>
9+
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
10+
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
11+
</PropertyGroup>
12+
</Project>

addon/chat/Chatio/Chatio.sln

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.9.34723.18
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Chatio", "Chatio.csproj", "{03BC4C39-81A2-4440-A442-2B93B0C68BFA}"
7+
EndProject
8+
Global
9+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
10+
Debug|Any CPU = Debug|Any CPU
11+
Release|Any CPU = Release|Any CPU
12+
EndGlobalSection
13+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
14+
{03BC4C39-81A2-4440-A442-2B93B0C68BFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15+
{03BC4C39-81A2-4440-A442-2B93B0C68BFA}.Debug|Any CPU.Build.0 = Debug|Any CPU
16+
{03BC4C39-81A2-4440-A442-2B93B0C68BFA}.Release|Any CPU.ActiveCfg = Release|Any CPU
17+
{03BC4C39-81A2-4440-A442-2B93B0C68BFA}.Release|Any CPU.Build.0 = Release|Any CPU
18+
EndGlobalSection
19+
GlobalSection(SolutionProperties) = preSolution
20+
HideSolutionNode = FALSE
21+
EndGlobalSection
22+
GlobalSection(ExtensibilityGlobals) = postSolution
23+
SolutionGuid = {73C44709-CDEF-4DF9-8E10-62DE730AA6DE}
24+
EndGlobalSection
25+
EndGlobal
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Chatio.Configurations;
2+
3+
public class MongoDbSettings
4+
{
5+
public string ConnectionString { get; set; } = null!;
6+
public string DatabaseName { get; set; } = null!;
7+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Chatio.Configurations;
2+
3+
public class OpenAiSettings
4+
{
5+
public required string ApiKey { get; set; }
6+
public required string DefaultChatModel { get; set; }
7+
public required string DefaultEmbeddingModel { get; set; }
8+
public double DefaultTemperature { get; set; }
9+
public double DefaultTopP { get; set; }
10+
public double DefaultFrequencyPenalty { get; set; }
11+
public double DefaultPresencePenalty { get; set; }
12+
public int DefaultMaxTokens { get; set; }
13+
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
using StackExchange.Redis;
2+
using System.Text.Json;
3+
using Chatio.Dtos;
4+
5+
namespace Chatio.Data.Hub;
6+
7+
public class HubRepository : IHubRepository
8+
{
9+
private readonly IDatabase _database;
10+
private const string UserConnectionsKey = "user_connections";
11+
private const string UserDetailsKey = "user_details";
12+
private const string RoomKey = "room";
13+
private const string RoomInfoKey = "room_info";
14+
private const string UserRoomsKey = "user_rooms";
15+
16+
public HubRepository(IConnectionMultiplexer redis)
17+
{
18+
_database = redis.GetDatabase();
19+
}
20+
21+
// Connection Management
22+
public async Task AddConnectionAsync(string connectionId, string userId, string username)
23+
{
24+
await _database.SetAddAsync($"{UserConnectionsKey}:{userId}", connectionId);
25+
26+
// Save user details with username
27+
var userDetails = new UserDetails
28+
{
29+
Id = userId,
30+
Username = username
31+
};
32+
await UpdateUserDetailsAsync(userId, userDetails);
33+
}
34+
35+
36+
public async Task RemoveConnectionAsync(string connectionId, string userId)
37+
{
38+
await _database.SetRemoveAsync($"{UserConnectionsKey}:{userId}", connectionId);
39+
40+
// Clean up if no more connections exist
41+
if (!(await _database.SetLengthAsync($"{UserConnectionsKey}:{userId}") > 0))
42+
{
43+
await _database.KeyDeleteAsync($"{UserConnectionsKey}:{userId}");
44+
// Don't remove user details as they might be needed later
45+
}
46+
}
47+
48+
public async Task<IEnumerable<string>> GetUserConnectionsAsync(string userId)
49+
{
50+
var connections = await _database.SetMembersAsync($"{UserConnectionsKey}:{userId}");
51+
return connections.Select(c => c.ToString());
52+
}
53+
54+
// User Management
55+
public async Task<UserDetails> GetUserDetailsAsync(string userId)
56+
{
57+
var userDetailsJson = await _database.StringGetAsync($"{UserDetailsKey}:{userId}");
58+
if (userDetailsJson.IsNull)
59+
{
60+
return new UserDetails
61+
{
62+
Id = userId,
63+
Username = $"User_{userId.Substring(0, 6)}" // Default if username not found
64+
};
65+
}
66+
return JsonSerializer.Deserialize<UserDetails>(userDetailsJson.ToString())!;
67+
}
68+
69+
70+
public async Task UpdateUserDetailsAsync(string userId, UserDetails details)
71+
{
72+
var json = JsonSerializer.Serialize(details);
73+
await _database.StringSetAsync($"{UserDetailsKey}:{userId}", json);
74+
}
75+
76+
// Room Management
77+
public async Task AddUserToRoomAsync(string roomId, string userId)
78+
{
79+
await _database.SetAddAsync($"{RoomKey}:{roomId}", userId);
80+
await _database.SetAddAsync($"{UserRoomsKey}:{userId}", roomId);
81+
}
82+
83+
public async Task RemoveUserFromRoomAsync(string roomId, string userId)
84+
{
85+
await _database.SetRemoveAsync($"{RoomKey}:{roomId}", userId);
86+
await _database.SetRemoveAsync($"{UserRoomsKey}:{userId}", roomId);
87+
88+
// Clean up empty room
89+
if (!(await _database.SetLengthAsync($"{RoomKey}:{roomId}") > 0))
90+
{
91+
await _database.KeyDeleteAsync($"{RoomKey}:{roomId}");
92+
await _database.KeyDeleteAsync($"{RoomInfoKey}:{roomId}");
93+
}
94+
}
95+
96+
public async Task<IEnumerable<string>> GetUsersInRoomAsync(string roomId)
97+
{
98+
var userIds = await _database.SetMembersAsync($"{RoomKey}:{roomId}");
99+
return userIds.Select(u => u.ToString());
100+
}
101+
102+
public async Task<IEnumerable<string>> GetUserRoomsAsync(string userId)
103+
{
104+
var roomIds = await _database.SetMembersAsync($"{UserRoomsKey}:{userId}");
105+
return roomIds.Select(r => r.ToString());
106+
}
107+
108+
public async Task<RoomInfo> GetRoomInfoAsync(string roomId)
109+
{
110+
var roomInfoJson = await _database.StringGetAsync($"{RoomInfoKey}:{roomId}");
111+
if (roomInfoJson.IsNull)
112+
{
113+
// Return default room info if not found
114+
return new RoomInfo
115+
{
116+
Id = roomId,
117+
Name = $"Room_{roomId}",
118+
Type = "group",
119+
CreatedAt = DateTime.UtcNow
120+
};
121+
}
122+
return JsonSerializer.Deserialize<RoomInfo>(roomInfoJson.ToString())!;
123+
}
124+
125+
public async Task UpdateRoomInfoAsync(string roomId, RoomInfo info)
126+
{
127+
var json = JsonSerializer.Serialize(info);
128+
await _database.StringSetAsync($"{RoomInfoKey}:{roomId}", json);
129+
}
130+
131+
// Helper methods for bulk operations
132+
public async Task<List<UserInfoDto>> GetParticipantsAsync(IEnumerable<string> userIds)
133+
{
134+
var participants = new List<UserInfoDto>();
135+
foreach (var userId in userIds)
136+
{
137+
var details = await GetUserDetailsAsync(userId);
138+
participants.Add(new UserInfoDto
139+
{
140+
Id = userId,
141+
Username = details.Username
142+
});
143+
}
144+
return participants;
145+
}
146+
public async Task<List<UserInfoDto>> GetRoomParticipantsAsync(string roomId)
147+
{
148+
// Get user IDs in the room
149+
var userIds = await GetUsersInRoomAsync(roomId);
150+
151+
// Get participant details for each user ID
152+
return await GetParticipantsAsync(userIds);
153+
}
154+
155+
// Optional: Methods for handling message history
156+
public async Task SaveMessageAsync(string roomId, MessageDto message)
157+
{
158+
var messageJson = JsonSerializer.Serialize(message);
159+
await _database.ListRightPushAsync($"messages:{roomId}", messageJson);
160+
// Optionally trim the list to maintain a maximum history
161+
await _database.ListTrimAsync($"messages:{roomId}", 0, 99); // Keep last 100 messages
162+
}
163+
164+
public async Task<List<MessageDto>> GetMessageHistoryAsync(string roomId, int count = 50)
165+
{
166+
var messages = await _database.ListRangeAsync($"messages:{roomId}", -count, -1);
167+
return messages
168+
.Select(m => JsonSerializer.Deserialize<MessageDto>(m.ToString())!)
169+
.ToList();
170+
}
171+
172+
public async Task<string?> GetUserIdByConnectionAsync(string connectionId)
173+
{
174+
var server = _database.Multiplexer.GetServer(_database.Multiplexer.GetEndPoints().First());
175+
176+
foreach (var key in server.Keys(pattern: $"{UserConnectionsKey}:*"))
177+
{
178+
if (await _database.SetContainsAsync(key, connectionId))
179+
{
180+
return key.ToString().Split(':').Last(); // Extract userId from the key
181+
}
182+
}
183+
184+
return null;
185+
}
186+
187+
public async Task<IEnumerable<string>> GetConnectionIdsByUserIdAsync(string userId)
188+
{
189+
var connections = await _database.SetMembersAsync($"{UserConnectionsKey}:{userId}");
190+
return connections.Select(c => c.ToString());
191+
}
192+
193+
}

0 commit comments

Comments
 (0)