Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 149 additions & 82 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ members = ["src/atlas_main", "src/atlas_space", "src/shared"]
resolver = "2"

[workspace.dependencies]
candid = "0.10"
ic-cdk = "0.18.5"
candid = "0.10.19"
ic-cdk = "0.18.7"
ic-management-canister-types = "0.3.1"
serde = "1.0.219"
serde_json = "1.0.140"
hex = "0.4.3"
ic-stable-structures = "0.6.8"
minicbor = { version = "0.26.4", features = ["alloc", "derive"] }
Expand All @@ -16,4 +18,4 @@ ic-ledger-types = "0.15.0"
ethnum = "1.5.1"
num-bigint = "0.4.6"
num-traits = "0.2.19"
sha2 = "0.10.9"
sha2 = "0.10.9"
1 change: 0 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

142 changes: 126 additions & 16 deletions src/atlas_frontend/src/canisters/atlasSpace/api.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,31 @@
import type { ActorSubclass } from "@dfinity/agent";
import type {
_SERVICE,
AnswerFormat,
ClosedTask,
EditTaskArgs,
State,
Submission,
SubmissionData,
Task,
TaskContent,
TwitterTaskType,
} from "../../../../declarations/atlas_space/atlas_space.did.js";
import { unwrapCall } from "../delegatedCall.js";
import { setSpace, setTasks } from "../../store/slices/spacesSlice.js";
import type { Dispatch } from "react";
import type { UnknownAction } from "@reduxjs/toolkit";
import type { Principal } from "@dfinity/principal";
import type { ExternalLinks } from "./types.js";
import type { DiscordTaskContent, GenericTaskContent, TwitterTaskContent } from "../../utils/taskMapper.js";
import type { DiscordGuild, DiscordInviteApiResponse } from "../../components/Integrations/discord/types.js";

export interface ExpiredTask extends Task {
expired: true;
}
import { validateDiscordInvite as validateInvite } from "../../components/Integrations/discord/inviteLink.js";
import { getUserGuilds } from "../../components/Integrations/discord/userGuilds.js";

interface CreateSubtaskArg {
task_type: string;
title: string;
description: string;
allow_resubmit: boolean;
answer_format: AnswerFormat
}
type CreateSubtaskArg = GenericTaskContent | DiscordTaskContent | TwitterTaskContent;

interface GetAtlasSpaceArgs {
unAuthAtlasSpace: ActorSubclass<_SERVICE>;
Expand Down Expand Up @@ -92,14 +91,38 @@ export const createNewTask = async ({
startTime,
endTime,
}: CreateNewSpaceTaskArgs) => {
const transformedTasks: TaskContent[] = tasks.map((arg) => ({
TitleAndDescription: {
task_title: arg.title,
task_description: arg.description,
allow_resubmit: arg.allow_resubmit,
answer_format: arg.answer_format
},
}));
const transformedTasks: TaskContent[] = tasks.map((arg) => {
if (arg.task_type === "discord") {
return {
DiscordTask: {
task_title: arg.title,
task_description: arg.description,
guild_id: arg.guild_id,
invite_link: arg.invite_link,
allow_resubmit: arg.allow_resubmit,
},
};
} else if (arg.task_type === "twitter") {
return {
TwitterTask: {
task_title: arg.title,
task_description: arg.description,
x_post_link: arg.x_post_link,
allow_resubmit: arg.allow_resubmit,
x_answer_format: arg.x_answer_format,
},
};
} else {
return {
TitleAndDescription: {
task_title: arg.title,
task_description: arg.description,
allow_resubmit: arg.allow_resubmit,
answer_format: arg.answer_format,
},
};
}
});

const call = authAtlasSpaceActor.create_task({
task_title: taskTitle,
Expand Down Expand Up @@ -436,3 +459,90 @@ export const deleteClosedTask = async ({
errMsg: "Failed to delete closed task",
});
};

export const getDiscordGuilds = async (
accessToken: string
): Promise<DiscordGuild[]> => {
return await getUserGuilds(accessToken);
};

export const validateDiscordInvite = async (
inviteCode: string,
expectedGuildId: string
): Promise<DiscordInviteApiResponse> => {
return await validateInvite(inviteCode, expectedGuildId);
};

interface ExchangeCodeForTokenArgs {
authAtlasSpace: ActorSubclass<_SERVICE>;
code: string;
codeVerifier: string;
}

export const exchange_code_for_token = async ({
authAtlasSpace,
code,
codeVerifier
}: ExchangeCodeForTokenArgs) => {
const call = authAtlasSpace.exchange_code_for_token(
code,
codeVerifier
);

return unwrapCall<String>({
call,
errMsg: "Failed to exchange Twitter code for token",
});
};

interface FetchXUserInfoArgs {
authAtlasSpace: ActorSubclass<_SERVICE>;
accessToken: string;
}

export const fetch_x_user_info = async ({
authAtlasSpace,
accessToken
}: FetchXUserInfoArgs) => {
const call = authAtlasSpace.fetch_x_user_info(
accessToken
);

return unwrapCall<String>({
call,
errMsg: "Failed to fetch X user info",
});
}

interface FetchXPostLikesArgs {
authAtlasSpace: ActorSubclass<_SERVICE>;
accessToken: string;
postId: string;
}

export async function fetch_x_tweet_activity(
args: FetchXPostLikesArgs,
taskType: TwitterTaskType
) {
const { authAtlasSpace, accessToken, postId } = args;

const call = authAtlasSpace.fetch_x_tweet_activity(
accessToken,
postId,
taskType
);

return unwrapCall<String>({
call,
errMsg: `Failed to fetch X post ${Object.keys(taskType)[0]}`,
});
}

export const fetch_x_post_likes = async (args: FetchXPostLikesArgs) => {
return fetch_x_tweet_activity(args, { 'Like' : null });
};

export const fetch_x_post_retweets = async (args: FetchXPostLikesArgs) => {
return fetch_x_tweet_activity(args, { 'Retweet' : null });
};

38 changes: 18 additions & 20 deletions src/atlas_frontend/src/canisters/atlasSpace/tasks.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,30 @@
import type { Principal } from "@dfinity/principal";
import type {
SubmissionData,
SubmissionState,
TaskType,
} from "../../../../declarations/atlas_space/atlas_space.did";
import type { UserSubmissionsData } from "./types";

export const getUsersSubmissions = (tasks: TaskType[]) => {
const data = tasks.reduce((acc, task, index) => {
if ("GenericTask" in task) {
const genericTask = task.GenericTask;
genericTask.submission.forEach(([principal, submissionData]) => {
const principalText = principal.toText();
if (!acc[principalText]) {
acc[principalText] = {};
}
if (!acc[principalText][`${index}`]) {
acc[principalText][`${index}`] = {
submissionData,
taskType: "GenericTask",
};
}
});
return acc;
}
export const getUsersSubmissions = (tasks: { [key: string]: TaskType }) => {
const data = Object.entries(tasks).reduce((acc, [subtaskIdStr, task]) => {
const foundType = Object.keys(task)[0] as keyof TaskType;
const taskData = task[foundType] as { submission: [Principal, SubmissionData][] };

taskData.submission.forEach(([principal, submissionData]) => {
const principalText = principal.toText();
if (!acc[principalText]) {
acc[principalText] = {};
}
acc[principalText][subtaskIdStr] = {
submissionData,
taskType: foundType,
};
});
return acc;
}, {} as UserSubmissionsData);

return new UserSubmissions(data);
};
}

export class UserSubmissions {
constructor(public userSubmissionsData: UserSubmissionsData) {}
Expand Down
9 changes: 0 additions & 9 deletions src/atlas_frontend/src/components/DiscordButton.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,29 +1,21 @@
import React, { useEffect } from "react";
import { useAuth } from "@nfid/identitykit/react";

const DiscordCallback = () => {
const { user } = useAuth();

useEffect(() => {
const query = new URLSearchParams(window.location.hash.substring(1));
const tokenType = query.get("token_type");
const accessToken = query.get("access_token");
const state = query.get("state");
const expiresIn = query.get("expires_in");

if (
!tokenType ||
!accessToken ||
!expiresIn ||
state === user?.principal.toString() ||
!window.opener
) {
return
}

try {
window.opener.postMessage(
{ tokenType, accessToken, state, expiresIn },
{ accessToken },
window.location.origin
);
} catch (err) {
Expand Down
28 changes: 28 additions & 0 deletions src/atlas_frontend/src/components/Integrations/TwitterCallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useEffect } from "react";

const TwitterCallback = () => {
useEffect(() => {
const qs = new URLSearchParams(window.location.search);
const payload = {
type: "x-oauth2-callback",
code: qs.get("code") || undefined,
state: qs.get("state") || undefined,
error: qs.get("error") || undefined,
error_description: qs.get("error_description") || undefined,
};

try {
if (window.opener && window.opener !== window) {
window.opener.postMessage(payload, window.location.origin);
setTimeout(() => window.close(), 50);
} else {
sessionStorage.setItem("x_oauth_payload", JSON.stringify(payload));
window.location.replace("/");
}
} catch {
window.location.replace("/");
}
}, []);
return null;
};
export default TwitterCallback;
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import axios from "axios";
import { DISCORD_CALLBACK_PATH } from "../router/paths";
import { DISCORD_CALLBACK_PATH } from "../../../router/paths";

export interface UserData {
accent_color: null | string;
Expand All @@ -21,12 +21,10 @@ export interface UserData {
username: string;
verified: boolean;
}

export const getOAuth2URL = (stateData?: string) => {
const discordBase = "https://discord.com";
const path = "/oauth2/authorize";
const url = new URL(path, discordBase);

url.searchParams.set("client_id", import.meta.env.PUBLIC_DISCORD_CLIENT_ID);
url.searchParams.set(
"redirect_uri",
Expand All @@ -35,10 +33,8 @@ export const getOAuth2URL = (stateData?: string) => {
url.searchParams.set("response_type", "token");
url.searchParams.set("scope", "identify");
if (stateData) url.searchParams.set("state", stateData);

return url.toString();
};

export const getUserData = async (token: string) => {
const { data } = await axios.get<UserData>(
"https://discord.com/api/users/@me",
Expand All @@ -48,6 +44,5 @@ export const getUserData = async (token: string) => {
},
}
);

return data;
};
};
Loading