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
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,31 @@ cd website && yarn dev

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

If icons from components-sdk are not loading for you Comment this alias in website/vite.config.ts

```bash
{find: /^components-sdk.*$/, replacement: resolve(__dirname, '../components-sdk/src')},
```

## 📦 Building


First, you need to have the steps above done

Build the website library:

```bash
cd website && yarn build
```

Run the build server of the website:

```bash
serve dist
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

## ⚠️ Commercial use

Although the `website/` project is licensed under the permissive MIT License, it depends on the `components-sdk/` package, which is **licensed under the PolyForm Noncommercial License 1.0.0**.
Expand Down
115 changes: 111 additions & 4 deletions website/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ function App() {
const stateManager = useMemo(() => new DisplaySliceManager(dispatch), [dispatch]);
const state = useSelector((state: RootState) => state.display.data)
const webhookUrl = useSelector((state: RootState) => state.display.webhookUrl);
const messageLink = useSelector((state: RootState) => state.display.messageUrl);
const response = useSelector((state: RootState) => state.display.webhookResponse);
const username = useSelector((state: RootState) => state.display.setusername);
const avatar = useSelector((state: RootState) => state.display.setavatar);
const [page, setPage] = useRouter();
const [postTitle, setPostTitle] = useState<string>("");
useHashRouter();
Expand All @@ -53,6 +56,26 @@ function App() {
return () => clearTimeout(getData)
}, [webhookUrl]);

useEffect(() => {
const getData = setTimeout(() => localStorage.setItem("discord.builders__messageLink", messageLink), 1000)
return () => clearTimeout(getData)
}, [messageLink]);

useEffect(() => {
document.querySelectorAll('._emoji_c7tgn_78').forEach(e => (e.childElementCount == 0) ? e.style.display = "none" : e.style.display = "");
});

let parsed_msg_url: URL | null = null;
try {
parsed_msg_url = new URL(messageLink);

if (parsed_msg_url.pathname.startsWith('/channels/') && parsed_msg_url.hostname === 'discord.com') {
parsed_msg_url.protocol = 'https:';
}

const parsed_query = new URLSearchParams(parsed_msg_url.search);
parsed_msg_url.search = parsed_query.toString();
} catch (e) {}

let parsed_url: URL | null = null;
try {
Expand All @@ -61,6 +84,9 @@ function App() {
if (parsed_url.pathname.startsWith('/api/webhooks/') && parsed_url.hostname === 'discord.com') {
parsed_url.protocol = 'https:';
parsed_url.pathname = '/api/v10/webhooks/' + parsed_url.pathname.slice('/api/webhooks/'.length);
if (parsed_msg_url != null) {
parsed_url.pathname += '/messages/'+parsed_msg_url.pathname.split('/').pop();
}
}

const parsed_query = new URLSearchParams(parsed_url.search);
Expand All @@ -75,10 +101,65 @@ function App() {
const threadId = useMemo(() => getThreadId(webhookUrl), [webhookUrl]);

const sendMessage = async () => {
const req = await fetch(String(parsed_url), webhookImplementation.prepareRequest(state))
var method_req;
let username_in = undefined;
let avatarurl_in = undefined;
if (parsed_msg_url != null) {method_req = "PATCH"}
else {
method_req = "POST";
if (username != "") username_in = username;
if (avatar != "") avatarurl_in = avatar;
}
const req = await fetch(String(parsed_url), webhookImplementation.prepareRequest(state, method_req, ...Array(1), username_in, avatarurl_in))

if (username == "" || avatar == ""){
const req_2 = await fetch(String(webhookUrl), webhookImplementation.prepareRequest(state, "GET"))
let data_2 = await req_2.json()
if (username == "") dispatch(actions.setUsernameData(data_2["name"]))
if (avatar == "") dispatch(actions.setAvatarData("https://cdn.discordapp.com/avatars/"+data_2["id"]+"/"+data_2["avatar"]+".png"))
}

const status_code = req.status;
if (status_code === 204) return dispatch(actions.setWebhookResponse({"status": "204 Success"}));
else if (status_code === 200) return dispatch(actions.setWebhookResponse({"status": "200 Success"}));

const error_data = await req.json();

if (error_data?.code === 220001 && dialog.current !== null) {
dialog.current.showModal();
dispatch(actions.setWebhookResponse(null));
return;
}

dispatch(actions.setWebhookResponse(error_data))
}

const getMessage = async () => {
const req = await fetch(String(parsed_url), webhookImplementation.prepareRequest(state, "GET"))

const status_code = req.status;
if (status_code === 204) return dispatch(actions.setWebhookResponse({"status": "204 Success"}))
else if (status_code === 200) {
let loaded_data = await req.json();
let reg_check = /"id":(([1-9]|[1-9][0-9]|[1-9][0-9][0-9]|[1-9][0-9][0-9][0-9])|"[^"]*"),/gm; //Discord includes ids in the component but this doesnt like the ids (it causes duplicates), this is to remove them
try {
var fixed_data = JSON.parse(JSON.stringify(loaded_data).replaceAll(reg_check,""));
} catch(err) {
console.error(err);
}
dispatch(actions.setComponentsData([]))
for (const comp of fixed_data["components"]){
let data_to_add = comp
if ("id" in data_to_add){
delete data_to_add.id;
}
dispatch(actions.appendKey({"key":["data"],"value":data_to_add}))
}
dispatch(actions.setUsernameData(loaded_data["author"]["username"]))
dispatch(actions.setAvatarData("https://cdn.discordapp.com/avatars/"+loaded_data["author"]["id"]+"/"+loaded_data["author"]["avatar"]+".png"))

return dispatch(actions.setWebhookResponse({"status": "200 Success"}))
}

const error_data = await req.json();

Expand All @@ -95,7 +176,7 @@ function App() {
if (!postTitle) return;
dialog.current?.close();

const req = await fetch(String(parsed_url), webhookImplementation.prepareRequest(state, postTitle))
const req = await fetch(String(parsed_url), webhookImplementation.prepareRequest(state, "POST", postTitle))

const status_code = req.status;
if (status_code === 204) return dispatch(actions.setWebhookResponse({"status": "204 Success"}));
Expand All @@ -114,7 +195,7 @@ function App() {
stateManager={stateManager}
stateKey={stateKey}
passProps={passProps}
className={Styles.preview}
className={Styles.input}
errors={errors}
/>
</ErrorBoundary>
Expand All @@ -135,7 +216,7 @@ function App() {
onChange={ev => dispatch(actions.setWebhookUrl(ev.target.value))}/>
</div>
<button className={Styles.button} disabled={parsed_url == null} onClick={sendMessage}>
Send
{((parsed_msg_url == null) ? 'Send' : 'Edit')}
</button>
</div>

Expand All @@ -146,6 +227,32 @@ function App() {
<input className={Styles.input} type="text" value={threadId || ""} onChange={ev => dispatch(actions.setThreadId(ev.target.value))} placeholder={"Optional. If you want to send the message to a thread, put the thread ID here."}/>
</div>

<div style={{marginBottom: '2rem'}}>
<p style={{marginBottom: '0.5rem'}}>Message Link</p>
<div className={Styles.input_pair}>
<div>
<input className={Styles.input} placeholder={"Optional. If you want to edit a message"} type="text" value={messageLink}
onChange={ev => dispatch(actions.setMessageLink(ev.target.value))}/>
</div>
<button className={Styles.button} disabled={parsed_msg_url == null || parsed_url == null} onClick={getMessage}>
Load
</button>
</div>
<p style={{marginTop: '0.5rem', marginBottom: '2rem', color: 'grey'}}>Warning: The message must to be sent by the webhook that edits it and uploading a image or a file doesnt work with editing.</p>
</div>

<div style={{marginBottom: '2rem'}}>
<div className={Styles.input_pair}>
<div>
<p style={{marginBottom: '0.5rem'}}>Username</p>
<input className={Styles.input} type="text" disabled={parsed_msg_url != null} value={username || ""} onChange={ev => dispatch(actions.setUsernameData(ev.target.value))} placeholder={((parsed_msg_url == null) ? 'Optional. If you want to change the username of the message.' : 'Cannot Change username in edit mode.')}/>
<p style={{marginBottom: '0.5rem', marginTop: '0px'}}>Avatar Url</p>
<input className={Styles.input} type="text" disabled={parsed_msg_url != null} value={avatar || ""} onChange={ev => dispatch(actions.setAvatarData(ev.target.value))} placeholder={((parsed_msg_url == null) ? 'Optional. If you want to change the avatar of the message.' : 'Cannot Change avatar in edit mode.')}/>
</div>
</div>
<p style={{marginTop: '0.5rem', marginBottom: '2rem', color: 'grey'}}>Warning: cant change name or profile when editing.</p>
</div>

<dialog ref={dialog} className={Styles.dialog}>
<form method="dialog"><button className={Styles.close}>✕</button></form>
<div>
Expand Down
25 changes: 15 additions & 10 deletions website/src/EmojiShow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,20 @@ export const EmojiShow: EmojiShowType = ({emoji}) => {
return getEmojiDataFromNative(emoji, 'twitter', data)
}, [])

if (emoji.id !== null) return <img alt={`Discord emoji: ${emoji.name}`}
src={`https://cdn.discordapp.com/emojis/${emoji.id}`}
width={19}
height={19}
/>
if (typeof(emoji) == "undefined"){
return null
}
else {
if (emoji.id !== null && typeof(emoji.id) !== "undefined") return <img alt={`Discord emoji${emoji.name}`}
src={`https://cdn.discordapp.com/emojis/${emoji.id}`}
width={19}
height={19}
/>

return <Emoji
set={'twitter'}
emoji={getEmojiData(emoji.name)}
size={19}
/>
return <Emoji
set={'twitter'}
emoji={getEmojiData(emoji.name)}
size={19}
/>
}
}
21 changes: 20 additions & 1 deletion website/src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ export const displaySlice = createSlice({
initialState: () => ({
data: [] as Component[],
webhookUrl: localStorage.getItem("discord.builders__webhookToken") || "", // Toolkit run this function so type is string
webhookResponse: null as object | null
webhookResponse: null as object | null,
messageUrl: localStorage.getItem("discord.builders__messageLink") || "", // Toolkit run this function so type is string
setusername: "",
setavatar: "",
}),
reducers: {
wrapKey(state, action: PayloadAction<wrapKeyType<any>>) {
Expand Down Expand Up @@ -150,6 +153,22 @@ export const displaySlice = createSlice({
state.webhookUrl = action.payload
},

setMessageLink(state, action: PayloadAction<string>) {
state.messageUrl = action.payload
},

setUsernameData(state, action: PayloadAction<string>) {
state.setusername = action.payload
},

setAvatarData(state, action: PayloadAction<string>) {
state.setavatar = action.payload
},

setComponentsData(state, action: PayloadAction<Array>) {
state.data = action.payload
},

setThreadId(state, action: PayloadAction<string>) {
try {
const parsed_url = new URL(state.webhookUrl);
Expand Down
11 changes: 7 additions & 4 deletions website/src/webhook.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,24 +58,27 @@ export const webhookImplementation = {
}
},

prepareRequest(state: Component[], thread_name?: string): RequestInit {
prepareRequest(state: Component[], method_req?: string, thread_name?: string, username?: string, avatar_url?: string): RequestInit {
const files = this.scrapFiles(state);

const data = JSON.stringify({
components: state,
flags: 32768,
thread_name,
username,
avatar_url,

});

if (!files.length) return {method: "POST", body: data, headers: {"Content-Type": "application/json"}}
if (!files.length) return {method: method_req, body: ((method_req == "GET") ? null : data), headers: {"Content-Type": "application/json"}}

const form = new FormData();
form.append('payload_json', data);
files.map((filename, idx) => {
const blob = window.uploadedFiles[filename];
form.append(`files[${idx}]`, blob, filename);
})
return {method: "POST", body: form, headers: {}}
return {method: method_req, body: form, headers: {}}
},

getErrors(response: unknown) {
Expand All @@ -91,4 +94,4 @@ export const webhookImplementation = {
return components as Record<string, any>;
}

}
}