Skip to content

Commit 164d994

Browse files
authored
feat: copy profile pictures to r2 bucket 🪣 (#841)
1 parent 74c28c4 commit 164d994

4 files changed

Lines changed: 118 additions & 17 deletions

File tree

packages/core/src/modules/linkedin.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from '@/modules/employment/employment.types';
1818
import { saveCompanyIfNecessary } from '@/modules/employment/use-cases/save-company-if-necessary';
1919
import { getMostRelevantLocation } from '@/modules/location/location';
20+
import { uploadProfilePicture } from '@/modules/members/use-cases/upload-profile-picture';
2021

2122
// Constants
2223

@@ -903,16 +904,19 @@ async function checkMember({ member, profile, trx }: CheckMemberInput) {
903904
return getMostRelevantLocation(locationFromLinkedIn, 'geocode');
904905
});
905906

907+
if (!!profile.element.photo && !profile.element.openToWork) {
908+
await uploadProfilePicture({
909+
memberId: member.id,
910+
pictureUrl: profile.element.photo,
911+
});
912+
}
913+
906914
return trx
907915
.updateTable('students')
908916
.set({
909917
...(!member.headline && {
910918
headline: profile.element.headline,
911919
}),
912-
...((!member.headline || !member.profilePicture) &&
913-
!profile.element.openToWork && {
914-
profilePicture: profile.element.photo,
915-
}),
916920
...(!!updatedLocation && {
917921
currentLocation: updatedLocation.formattedAddress,
918922
currentLocationCoordinates: point({
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { db } from '@oyster/db';
2+
import { id } from '@oyster/utils';
3+
4+
import {
5+
deleteObject,
6+
putObject,
7+
R2_PUBLIC_BUCKET_NAME,
8+
R2_PUBLIC_BUCKET_URL,
9+
} from '@/infrastructure/s3';
10+
11+
type UploadProfilePictureInput = {
12+
memberId: string;
13+
pictureUrl: string;
14+
};
15+
16+
/**
17+
* Uploads a profile picture to S3 and updates the member's profile picture in
18+
* the database. It first fetches the picture from the URL (either coming
19+
* from LinkedIn or Slack) and effecitvielly "copies" the contents to S3.
20+
*
21+
* @param input - The input for the upload profile picture use case.
22+
* @returns The updated member's profile picture.
23+
*/
24+
export async function uploadProfilePicture({
25+
memberId,
26+
pictureUrl,
27+
}: UploadProfilePictureInput) {
28+
const member = await db
29+
.selectFrom('students')
30+
.select(['profilePicture', 'profilePictureKey'])
31+
.where('id', '=', memberId)
32+
.executeTakeFirst();
33+
34+
if (!member) {
35+
return;
36+
}
37+
38+
const response = await fetch(pictureUrl);
39+
40+
if (!response.ok) {
41+
return;
42+
}
43+
44+
const arrayBuffer = await response.arrayBuffer();
45+
const buffer = Buffer.from(arrayBuffer);
46+
47+
const contentType = response.headers.get('content-type');
48+
49+
const extension = contentType?.includes('image/')
50+
? contentType.split('/')[1]
51+
: null;
52+
53+
const key = extension ? `members/${id()}.${extension}` : `members/${id()}`;
54+
55+
await putObject({
56+
bucket: R2_PUBLIC_BUCKET_NAME,
57+
content: buffer,
58+
contentType: contentType || undefined,
59+
key,
60+
});
61+
62+
await db
63+
.updateTable('students')
64+
.set({
65+
profilePicture: `${R2_PUBLIC_BUCKET_URL}/${key}`,
66+
profilePictureKey: key,
67+
})
68+
.where('id', '=', memberId)
69+
.execute();
70+
71+
if (member.profilePictureKey) {
72+
await deleteObject({
73+
bucket: R2_PUBLIC_BUCKET_NAME,
74+
key: member.profilePictureKey,
75+
});
76+
}
77+
78+
return {
79+
profilePicture: `${R2_PUBLIC_BUCKET_URL}/${key}`,
80+
profilePictureKey: key,
81+
};
82+
}

packages/core/src/modules/slack/events/slack-profile-picture-changed.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,34 @@ import { db } from '@oyster/db';
22

33
import { job } from '@/infrastructure/bull';
44
import { type GetBullJobData } from '@/infrastructure/bull.types';
5+
import { uploadProfilePicture } from '@/modules/members/use-cases/upload-profile-picture';
56

67
export async function onSlackProfilePictureChanged({
78
profilePicture,
89
slackId,
910
}: GetBullJobData<'slack.profile_picture.changed'>) {
10-
const student = await db
11+
if (!profilePicture) {
12+
return;
13+
}
14+
15+
const member = await db
1116
.selectFrom('students')
1217
.select(['id', 'profilePicture'])
1318
.where('slackId', '=', slackId)
1419
.executeTakeFirst();
1520

16-
if (!student) {
17-
return;
18-
}
19-
20-
if (profilePicture === student.profilePicture) {
21+
if (!member) {
2122
return;
2223
}
2324

24-
await db
25-
.updateTable('students')
26-
.set({ profilePicture })
27-
.where('id', '=', student.id)
28-
.execute();
25+
const updatedMember = await uploadProfilePicture({
26+
memberId: member.id,
27+
pictureUrl: profilePicture,
28+
});
2929

30-
if (!student.profilePicture && profilePicture) {
30+
if (!member.profilePicture && updatedMember?.profilePicture) {
3131
job('gamification.activity.completed', {
32-
studentId: student.id,
32+
studentId: member.id,
3333
type: 'upload_profile_picture',
3434
});
3535
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { type Kysely } from 'kysely';
2+
3+
export async function up(db: Kysely<any>) {
4+
await db.schema
5+
.alterTable('students')
6+
.addColumn('profile_picture_key', 'text')
7+
.execute();
8+
}
9+
10+
export async function down(db: Kysely<any>) {
11+
await db.schema
12+
.alterTable('students')
13+
.dropColumn('profile_picture_key')
14+
.execute();
15+
}

0 commit comments

Comments
 (0)