Skip to content
This repository has been archived by the owner on Mar 31, 2024. It is now read-only.

Commit

Permalink
v0.2.1 merge
Browse files Browse the repository at this point in the history
* use console.debug

* v0.2.0
  - bug fixes
  - all (""useful"") rpc commands implemented
  - added close method to close the Embedded Activity

* README example

* 🔖 v0.2.0

* typescript & bug fixes

* updated gitignore

* package.json

* use console.debug

* README example

* 🔖 v0.2.0

* 📄 copyright

* ♻️ rebase merge conflict

* ♻️✨ authorize & authenticate commands

* package.json (v0.2.1)

---------

Co-authored-by: derpystuff <[email protected]>
  • Loading branch information
UserUNP and derpystuff committed Mar 16, 2023
1 parent d2fa76e commit df9806b
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 71 deletions.
1 change: 1 addition & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
MIT License

Copyright (c) 2023 big nutty
Copyright (c) 2023 UserUNP

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
36 changes: 33 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,36 @@
# ActivitySDK
![npm](https://img.shields.io/npm/v/activitysdk)

Unofficial SDK to communicate between the discord client and an embedded activity.
| activitysdk-ts | activitysdk |
| :------------: | :---------: |
| ![npm](https://img.shields.io/npm/v/activitysdk-ts) | ![npm](https://img.shields.io/npm/v/activitysdk) |

Documentation and examples will be coming soon.
Typescript fork of [activitysdk](http://npmjs.org/activitysdk).

**Example usage:**
_you **need** to use a bundler such as [vite](https://vitejs.dev/) or [webpack](https://webpack.js.org/) or any bundler you like._
```js
import ActivitySDK from "activitysdk-ts";

const sdk = new ActivitySDK("app id");

sdk.on("READY", async () => {
let user;
try {
user = (await sdk.commands.authenticate("app secret", ["identify"])).user;
} catch (e) {
console.error("could not authorize/authenticate");
return;
}

const info = document.createElement("div");
info.style.backgroundColor = "gray";
info.style.display = "inline-block";

const username = document.createElement("p");
username.innerText = user.username;
username.style.color = "cyan";

info.append("Hello ", username);
document.body.append(info);
});
```
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "activitysdk",
"version": "0.2.0",
"description": "Unofficial SDK to communicate between the discord client and an embedded activity.",
"name": "activitysdk-ts",
"version": "0.2.1",
"description": "Typescript fork of activitysdk.",
"main": "./dist/lib/index.js",
"types": "./dist/types/index.d.ts",
"dependencies": {
Expand All @@ -21,7 +21,7 @@
"./dist", "./src",
"./README.md", "./LICENSE"
],
"author": "big nutty",
"author": "UserUNP",
"license": "MIT",
"bugs": {
"url": "https://github.com/UserUNP/activitysdk-ts/issues"
Expand Down
51 changes: 38 additions & 13 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
export enum OPCODES { //* same as the object you had.
"HANDSHAKE",
"FRAME",
"CLOSE",
"HELLO",
HANDSHAKE,
FRAME,
CLOSE,
HELLO,
};

export const PLATFORM_TYPES = [
export enum ORIENTATION_LOCK_STATES {
UNLOCKED = 1,
PORTRAIT,
LANDSCAPE
};

export const PLATFORM_TYPES = Object.freeze([
"desktop",
"mobile"
] as const;
] as const);

export const RPC_EVENTS = [
export const DEFAULT_AUTH_SCOPES = Object.freeze([
"identify", "rpc",
"guilds", "guilds.members.read"
] as const);

export const RPC_EVENTS = Object.freeze([
"CURRENT_USER_UPDATE",
"GUILD_STATUS",
"GUILD_CREATE",
Expand Down Expand Up @@ -53,10 +64,12 @@ export const RPC_EVENTS = [
"VOICE_CHANNEL_EFFECT_RECENT_EMOJI",
"VOICE_CHANNEL_EFFECT_TOGGLE_ANIMATION_TYPE",
"READY",
"ERROR"
] as const;
"ERROR",
"SET_ORIENTATION_LOCK_STATE",
"GET_PLATFORM_BEHAVIORS"
] as const);

export const RPC_COMMANDS = [
export const RPC_COMMANDS = Object.freeze([
"DISPATCH",
"SET_CONFIG",
"AUTHORIZE",
Expand Down Expand Up @@ -130,7 +143,7 @@ export const RPC_COMMANDS = [
"CAPTURE_LOG",
"ENCOURAGE_HW_ACCELERATION",
"SET_ORIENTATION_LOCK_STATE"
] as const;
] as const);

//* pov: ottelino hates typescript
//* (something) as const -> infers the type as the literal of this object -> makes it readonly.
Expand All @@ -142,11 +155,12 @@ export const RPC_COMMANDS = [
//? interface -> object type but compiles faster.
//? you can have functions that work as a type guard

export type OrientationState = "unlocked" | "portrait" | "landscape";
export type PlatformType = (typeof PLATFORM_TYPES)[number];
export type RPCEvent = (typeof RPC_EVENTS)[number];
export type RPCCommand = (typeof RPC_COMMANDS)[number];
export interface RPCPayload<DataObj extends object> {
cmd: RPCCommand;
cmd: RPCCommand | null;
evt: RPCEvent | null;
nonce: string | null;
data?: DataObj; // sometimes its type of string for some reason.
Expand All @@ -161,6 +175,10 @@ export function malformedResponseError() {
new Error("Malformed response from server.");
}

export function debug(...message: any[]) {
console.debug("[ActivitySDK]", ...message);
}

export function isPlatform(platform: unknown): platform is PlatformType {
if (!platform) return false;
if (typeof platform !== "string") return false;
Expand All @@ -182,10 +200,17 @@ export function isCommand(command: unknown): command is RPCCommand {
else return true;
}

export function isMessage(message: unknown): message is RPCPayload<object> {
export function isPayload(message: unknown): message is RPCPayload<object> {
if (!message) return false;
if (typeof message !== "object") return false;
if (!("cmd" in message && "nonce" in message && "data" in message)) return false;
else return true;
}

export function isOrientationState(orientationCode: unknown): orientationCode is ORIENTATION_LOCK_STATES {
if (!orientationCode) return false;
if (typeof orientationCode !== "number") return false;
if (!ORIENTATION_LOCK_STATES[orientationCode]) return false;
else return true;
}

93 changes: 65 additions & 28 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ import EventEmitter from 'eventemitter3';
import { v4 as uuid } from 'uuid';

import {
debug,
malformedResponseError, malformedRequestError,

OPCODES,
DEFAULT_AUTH_SCOPES,

PlatformType, isPlatform,
RPCCommand, isCommand,
RPCEvent, isEvent,
RPCPayload,
RPCPayload, isPayload,
} from './constants';
import ActivitySDKCommands from './rpc/command';

Expand All @@ -16,8 +21,8 @@ class ActivitySDK extends EventEmitter<RPCEvent> {
rpcTarget: Window = window.parent.opener || window.parent;
rpcOrigin: string = document.referrer || "*";

commands = new ActivitySDKCommands(this.appId, this.appSecret, this.sendCommand.bind(this));
commandCache = new Map<string, { resolve: (value: RPCPayload<object>["data"]) => void; reject: (reason?: any) => void }>();
commands = new ActivitySDKCommands(this.appId, this.sendCommand.bind(this));
commandCache = new Map<string, { resolve: (value?: object) => void; reject: (reason?: any) => void }>();

frameId: string;
instanceId: string;
Expand All @@ -26,7 +31,7 @@ class ActivitySDK extends EventEmitter<RPCEvent> {
guildId: string;


constructor(public appId: string, public appSecret: string) {
constructor(public appId: string) {
super();

window.addEventListener('message', this._handleMessage.bind(this));
Expand All @@ -43,21 +48,26 @@ class ActivitySDK extends EventEmitter<RPCEvent> {

if (!frameParam) throw new Error("Window missing frame_id"); else this.frameId = frameParam;
if (!instanceParam) throw new Error("Window missing instance_id"); else this.instanceId = instanceParam;
if (!channelParam) throw new Error("Window missing frame_id"); else this.channelId = channelParam;
if (!guildParam) throw new Error("Window missing frame_id"); else this.guildId = guildParam;
if (!channelParam) throw new Error("Window missing channel_id"); else this.channelId = channelParam;
if (!guildParam) throw new Error("Window missing guild_id"); else this.guildId = guildParam;

this._handshake();
}

this._init();
async login(appSecret: string, scopes: readonly string[] = DEFAULT_AUTH_SCOPES, consent: boolean = false) {
const authorizeRes = await this.commands.authorize(scopes, consent);
if (!authorizeRes || !("code" in authorizeRes) || typeof authorizeRes.code !== "string") throw malformedResponseError();
const authorizationGrant = await utils.exchangeAuthorizationCode(this.appId, appSecret, authorizeRes.code);
if (!authorizationGrant || "error" in authorizationGrant || !("access_token" in authorizationGrant) || typeof authorizationGrant.access_token !== "string") throw malformedRequestError();
return await this.commands.authenticate(authorizationGrant.access_token);
}

private _handleMessage(message: MessageEvent) {
if (typeof message.data !== "object") {
console.log("Recieved message: ", message.data);
return;
}
if (typeof message.data !== "object") return debug("Recieved message: ", message.data);
const opcode = message.data[0];
const payload = message.data[1];
if (!(opcode in OPCODES)) throw new Error("Invalid opcode recieved: " + opcode);
console.log("Recieved opcode " + OPCODES[opcode], message);
debug("Recieved opcode " + OPCODES[opcode], message);
switch (OPCODES[opcode]) {
case "HANDSHAKE":
return;
Expand All @@ -70,16 +80,16 @@ class ActivitySDK extends EventEmitter<RPCEvent> {
this.isReady = true;
return;
default:
throw "Unable to handle opcode: " + OPCODES[opcode];
throw new Error("Unable to handle opcode: " + OPCODES[opcode]);
}
}

private _postRawPayload(opcode: OPCODES, payload: RPCPayload<object>) {
console.log("post " + OPCODES[opcode] + " with", payload);
_postRawPayload(opcode: OPCODES, payload: object) {
debug("Posting " + OPCODES[opcode] + " with", payload);
this.rpcTarget.postMessage([opcode, payload], this.rpcOrigin);
}

sendCommand<DataObj extends object>(type: RPCCommand, args: Required<RPCPayload<object>["args"]>): Promise<RPCPayload<DataObj>["data"]> {
sendCommand<DataObj extends object>(type: RPCCommand, args: Required<RPCPayload<object>["args"]>): Promise<DataObj> {
if (!isCommand(type)) throw new Error("Invalid RPC Command: " + type);
const nonce = uuid();

Expand All @@ -88,7 +98,12 @@ class ActivitySDK extends EventEmitter<RPCEvent> {
cmd: type, evt: null,
});

return new Promise<RPCPayload<object>["data"]>((resolve, reject) => this.commandCache.set(nonce, { resolve, reject })) as Promise<RPCPayload<DataObj>["data"]>;
return new Promise<RPCPayload<object>["data"]>((resolve, reject) => this.commandCache.set(nonce, { resolve, reject })) as Promise<DataObj>;
}

close(data: { code: number; message: string }) {
window.removeEventListener('message', this._handleMessage);
this._sendData(OPCODES.CLOSE, { code: data.code, message: data.message })
}

postPayload(payload: Omit<RPCPayload<object>, "nonce">) {
Expand All @@ -104,23 +119,36 @@ class ActivitySDK extends EventEmitter<RPCEvent> {
return new Promise((resolve, reject) => this.commandCache.set(nonce, { resolve, reject }));
}

_sendData(opcode: Exclude<OPCODES, "FRAME" | 1>, data: object) {
const nonce = uuid();
this._postRawPayload(opcode, {nonce, ...data});
}

// Command Handling
private _resolveCommand<DataObj extends object>(nonce: string, data: Required<RPCPayload<DataObj>["data"]>) {
private _resolveCommand<DataObj extends object>(nonce: string, data?: DataObj) {
const response = this.commandCache.get(nonce);
if (response) response.resolve(data);

this.commandCache.delete(nonce);
}

private _rejectCommand<DataObj extends object>(nonce: string, data: Required<RPCPayload<DataObj>["data"]>) {
private _rejectCommand<DataObj extends object>(nonce: string, data?: DataObj) {
const response = this.commandCache.get(nonce);
if (response) response.reject(data);

this.commandCache.delete(nonce);
if (!data || !isPayload(data)) return;
if (!data.evt || !data.data) throw malformedResponseError();
else if (data.cmd == "DISPATCH") {
this.emit(data.evt, data.data);
return;
}
if (!data.nonce) throw new Error("Missing nonce");
else if (data.evt == "ERROR") this._rejectCommand(data.nonce, data.data);
else this._resolveCommand(data.nonce, data.data);

}

private _handleFrame(resp: RPCPayload<object>) {
console.log("Handle frame (response) -> ", resp);
debug("Handle frame (response) -> ", resp);
if (resp.cmd === "DISPATCH") {
if (!isEvent(resp.evt)) throw new Error("Undefined event in DISPATCH command.");
this.emit(resp.evt, resp.data);
Expand All @@ -133,11 +161,11 @@ class ActivitySDK extends EventEmitter<RPCEvent> {
};

// Handling RPC Events
async subscribe(type: RPCEvent, reciever: () => void) {
async subscribe(type: RPCEvent, args: object, reciever: () => void) {
if (!isEvent(type)) throw new Error("Invalid RPC Event: " + type);

await this.postPayload({
evt: type, cmd: "SUBSCRIBE"
evt: type, cmd: "SUBSCRIBE", args
});

return this.on(type, reciever);
Expand Down Expand Up @@ -185,10 +213,19 @@ class ActivitySDK extends EventEmitter<RPCEvent> {
], this.rpcOrigin);
}

private _init() {
this._handshake();
}

}

export const utils = {
async exchangeAuthorizationCode(appId: string, appSecret: string, code: string) {
return await (await fetch("https://discord.com/api/oauth2/token", {
method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({
grant_type: "authorization_code",
code: code,
client_id: appId,
client_secret: appSecret
})
})).json() as object;
},
} as const;

export default ActivitySDK;
Loading

0 comments on commit df9806b

Please sign in to comment.