-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f428573
commit 5c81148
Showing
11 changed files
with
492 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<!DOCTYPE html> | ||
|
||
<body> | ||
<h1>There has been an error in the request :(</h1> | ||
<script> | ||
const search = new URLSearchParams(window.location.search); | ||
const [code, state] = [search.get("code"), search.get("state")]; | ||
if (code && state) { | ||
window.opener.postMessage({ code, state }, window.location.origin); | ||
close(); | ||
} | ||
</script> | ||
</body> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
# TikTok Server | ||
|
||
Since TikTok API requries that photo upload is done through an URL, you'll need | ||
a server to handle this. Here's a server example that you can use with Deno | ||
Deploy. | ||
|
||
## Setup: | ||
|
||
### Getting an API Key | ||
|
||
You'll need an API Key to upload content on TikTok. | ||
|
||
1. Go to the | ||
[TikTok for Developers' website](https://developers.tiktok.com/apps/). You | ||
might need to create an account. | ||
2. Go into the [Manage apps](https://developers.tiktok.com/apps/) section. | ||
3. Click to the "Connect an app" button, and write the app name. | ||
4. Now go to the "Sandbox" tab, and then click the red "Create Sandbox" button. | ||
Add whatever name you like. | ||
5. Fill the form of the "Basic information" card. Check "Web" in the platforms. | ||
Currently, don't add anything in the URLs. | ||
|
||
### Deno Deploy | ||
|
||
6. Create a new serverless project in Deno Deploy, and copy the `server.ts` | ||
content. | ||
7. You now need to add some environment values: click on the option button (at | ||
the left of the "TS" select) and write the following values | ||
- `clientKey`: the Client Key that you can access from the TikTok dashboard | ||
in the "Credentials" tab | ||
- `clientSecret`: same as above, but for the Client Secret | ||
- `denoUrl`: the URL where the Deno project is deployed. Put it without "/" | ||
- `sourceUrl`: the URL where your image-converter webpage is located. Put it | ||
without "/" | ||
8. Save the values. Copy again the URL of your Deno project, and add it where | ||
required in the TikTok form. | ||
|
||
### Add the products on TikTok | ||
|
||
9. On the TikTok Developers page, click to "Add products", and choose "Login | ||
Kit" and "Content Posting API" | ||
10. Write again the Deno URL in the "Redirect URI" textbox in the "Login Kit" | ||
card | ||
11. Enable "Direct Post" on the "Content Posting API" card | ||
12. Now click on the "Verify" button of the "Content Posting API" card. | ||
13. Choose "sandbox1" from the "URL properties for" select, and click on "Verify | ||
new property" | ||
14. Choose "URL prefix", write again the URL of the Deno Deploy server (this | ||
time, add a "/"), and press on "Verify". Download the file. | ||
15. Go back to the Deno Deploy project, and add two new environment values: | ||
- `tiktokVerificationFileName`: the name of the file you've downloaded (with | ||
also the extension) | ||
- `tiktokVerificationFileContent`: the content of the file you've downloaded | ||
16. Save the values. Now verify the URL on the TikTok page. | ||
17. In the Sandbox settings, login with your TikTok account. | ||
|
||
### Compatibility on image-converter | ||
|
||
You are now ready to use the image-converter integration to TikTok! | ||
|
||
18. On image-converter, open the settings. | ||
19. Write the Deno URL (with the final "/") in the "TikTok integration" card |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
|
||
const kv = await Deno.openKv(); | ||
kv.listenQueue(async (value: string) => { // Time to delete the content | ||
const list = (await kv.get(["availableImages", value])).value as number ?? 0; // The maximum "position" number | ||
for (let i = 1; i < list + 1; i++) { // Items start to 1 | ||
const getBuffersAvailable = (await kv.get(["availableImages", value, i.toString()])).value as number ?? 0; // Get the number of divided buffers | ||
for (let x = 0; x < getBuffersAvailable; x++) await kv.delete(["availableImages", value, i.toString(), x]); // Iterate on each buffer to delete them | ||
await kv.delete(["availableImages", value, i.toString()]); | ||
} | ||
await kv.delete(["availableImages", value]); | ||
await kv.delete(["availableIds", value]); | ||
}) | ||
interface AutoResponse { | ||
body?: BodyInit, | ||
status?: number, | ||
type?: string | ||
} | ||
interface PostRequest { | ||
id: string, | ||
code: string, | ||
description: string, | ||
title: string, | ||
thumbnail: number | ||
} | ||
/** | ||
* Make a Response with CORS configured | ||
* @param body the body of the request | ||
* @param status the status of the response | ||
* @param type the "Content-Type" added in the header | ||
* @returns a Response to return to the request | ||
*/ | ||
function autoResponse({ body, status, type }: AutoResponse) { | ||
return new Response(body, { | ||
status: status, | ||
headers: { | ||
"Content-Type": type ?? "text/plain", | ||
"Access-Control-Allow-Origin": Deno.env.get("sourceUrl") ?? "http://localhost:5173", // TODO: Change it with only the production website! | ||
"Access-Control-Allow-Methods": "GET, POST", | ||
"Access-Control-Allow-Headers": "Authorization", | ||
|
||
} | ||
}); | ||
} | ||
/** | ||
* Divide an ArrayBuffer in an array of ArrayBuffers of a smaller length, so that they can be stored on Deno KV | ||
* @param buffer the buffer to divide | ||
* @param chunkSize the size of the chunk | ||
* @returns an ArrayBuffer[], of a fixed length | ||
*/ | ||
function chunkArrayBuffer(buffer: ArrayBuffer, chunkSize: number) { | ||
const chunked = []; | ||
let offset = 0; | ||
while (offset < buffer.byteLength) { | ||
const size = Math.min(chunkSize, buffer.byteLength - offset); | ||
const chunk = buffer.slice(offset, offset + size); | ||
chunked.push(chunk); | ||
offset += size; | ||
} | ||
return chunked; | ||
} | ||
/** | ||
* Join multiple ArrayBuffers in a single one | ||
* @param buffers the Buffers to join | ||
* @returns the joined ArrayBuffer | ||
*/ | ||
function joinArrayBuffers(buffers: ArrayBuffer[]) { | ||
const size = buffers.reduce((acc, buffer) => acc + buffer.byteLength, 0); | ||
const finalBuffer = new ArrayBuffer(size); | ||
const array = new Uint8Array(finalBuffer); | ||
let offset = 0; | ||
for (const buffer of buffers) { | ||
array.set(new Uint8Array(buffer), offset); | ||
offset += buffer.byteLength; | ||
} | ||
return finalBuffer; | ||
} | ||
|
||
Deno.serve(async (req) => { | ||
if (req.method.toLowerCase() === "options") return autoResponse({ body: "", status: 204 }); // Request sent by the browser for CORS. There's no need to continue. | ||
let url = req.url.substring(req.url.lastIndexOf("/") + 1); | ||
if (url.indexOf("?") !== -1) url = url.substring(0, url.indexOf("?")); | ||
switch (url) { | ||
case "getId": { // Get a random ID that'll be used for identifying the uploaded images | ||
const ids = crypto.randomUUID(); | ||
await kv.set(["availableIds", ids], Date.now()); | ||
kv.enqueue(ids, { delay: 600000 }); // Delete everything after 10 minutes | ||
return autoResponse({ status: 200, body: JSON.stringify({ id: ids }) }); | ||
} | ||
case "upload": { // Upload image to the server | ||
if (req.method !== "POST") return autoResponse({ body: "Endpoint available only with POST request.", status: 405 }); | ||
const params = new URLSearchParams(req.url.substring(req.url.indexOf("?") + 1)); | ||
const getPosition = params.get("position"); | ||
const getId = params.get("id"); | ||
if (getPosition && getId && !isNaN(+getPosition) && +getPosition > 0 && getPosition.indexOf(".") === -1) { // getPosition must be a positive integer, since this is the way we'll keep track of images | ||
if (!(await kv.get(["availableIds", getId])).value) return autoResponse({ body: "Request expired.", status: 403 }); | ||
const previousPosition = (await kv.get(["availableImages", getId])).value as number ?? 0; | ||
const buffer = chunkArrayBuffer(await new Response(req.body).arrayBuffer(), 50 * 1024); // Divide the buffers with a maximum size of 50kb so that they can be stored in Deno KV. | ||
await kv.set(["availableImages", getId, getPosition], buffer.length); | ||
for (let i = 0; i < buffer.length; i++) await kv.set(["availableImages", getId, getPosition, i], buffer[i]); | ||
await kv.set(["availableImages", getId], Math.max(previousPosition, +getPosition)); | ||
return autoResponse({ body: "OK", status: 200 }); | ||
} | ||
return autoResponse({ body: "Missing fields", status: 400 }) | ||
} | ||
case "fetch": { // Get the image content | ||
const params = new URLSearchParams(req.url.substring(req.url.indexOf("?") + 1)); | ||
const getPosition = params.get("position"); | ||
const getId = params.get("id"); | ||
if (getPosition && getId) { | ||
const generalInfo = await kv.get(["availableImages", getId, getPosition]); | ||
if (typeof generalInfo.value === "number") { // There are some ArrayBuffers of the image | ||
const mergedArr: ArrayBuffer[] = []; | ||
for (let i = 0; i < generalInfo.value; i++) { // Get each ArrayBuffer and merge them | ||
const output = await kv.get(["availableImages", getId, getPosition, i]); | ||
output.value instanceof ArrayBuffer && mergedArr.push(output.value); | ||
} | ||
const outputBuffer = joinArrayBuffers(mergedArr); | ||
return autoResponse({ body: outputBuffer as ArrayBuffer, status: 200 }); | ||
} else return autoResponse({ body: "Not found", status: 404 }); | ||
} else return autoResponse({ body: "Missing fields", status: 400 }); | ||
} | ||
case "auth": { // Start the authentication process using OAuth | ||
const state = new URLSearchParams(req.url.substring(req.url.indexOf("?") + 1)).get("state"); | ||
if (!state) return autoResponse({ body: "Missing fields", status: 40 }) | ||
return autoResponse({ body: `<!DOCTYPE html><body><script>window.location.href = "https://www.tiktok.com/v2/auth/authorize?client_key=${Deno.env.get("clientKey")}&redirect_uri=${encodeURIComponent(Deno.env.get("sourceUrl") + "/oauth.html")}&state=${encodeURIComponent(state)}&response_type=code&scope=${encodeURIComponent("video.publish,video.upload,user.info.basic,user.info.basic")}"</script></body>`, type: "text/html" }) | ||
} | ||
case "post": { // Send the request to post the content to TikTok servers | ||
if (req.method !== "POST") return autoResponse({ body: "Endpoint available only with POST request.", status: 405 }); | ||
const json = await new Response(req.body).json() as PostRequest; | ||
if (typeof json.description === "string" && typeof json.code === "string" && typeof json.id === "string" && typeof json.title === "string" && typeof json.thumbnail === "number") { | ||
const tokenRequest = await fetch(`https://open.tiktokapis.com/v2/oauth/token/`, { | ||
headers: { | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
method: "POST", | ||
body: `client_key=${Deno.env.get("clientKey")}&client_secret=${Deno.env.get("clientSecret")}&code=${json.code}&grant_type=authorization_code&redirect_uri=${encodeURIComponent(Deno.env.get("sourceUrl") + "/oauth.html")}` | ||
}); | ||
const tokenJson = await tokenRequest.json(); | ||
if (tokenRequest.status === 200) { | ||
const buildImageUrl = (await kv.get(["availableImages", json.id])).value; | ||
if (!buildImageUrl) return autoResponse({ body: "Non-existent ID", status: 401 }); | ||
const finalBuild = Math.min(buildImageUrl as number, 35); | ||
const finalArr = []; | ||
for (let i = 1; i < finalBuild + 1; i++) finalArr.push(`${Deno.env.get("denoUrl")}/fetch?id=${json.id}&position=${i}`); | ||
const imageRequest = await fetch("https://open.tiktokapis.com/v2/post/publish/content/init/", { | ||
headers: { | ||
Authorization: `Bearer ${tokenJson.access_token}`, | ||
"Content-Type": `application/json; charset=UTF-8` | ||
}, | ||
method: "POST", | ||
body: JSON.stringify({ | ||
media_type: "PHOTO", | ||
post_mode: "DIRECT_POST", | ||
post_info: { | ||
title: json.title, | ||
description: json.description, | ||
privacy_level: "SELF_ONLY", | ||
}, | ||
source_info: { | ||
source: "PULL_FROM_URL", | ||
photo_images: finalArr, | ||
photo_cover_index: json.thumbnail | ||
} | ||
}) | ||
}) | ||
if (imageRequest.status === 200) return autoResponse({ body: "", status: 200 }); else return autoResponse({ body: "Error while uploading images", status: 500 }); | ||
}; | ||
return autoResponse({ body: "Error while fetching token", status: 500 }); | ||
} else return autoResponse({ body: "Missing fields" }) | ||
} | ||
case Deno.env.get("tiktokVerificationFileName"): { // The file that TikTok requires to keep in the server to verify ownership | ||
return autoResponse({ body: Deno.env.get("tiktokVerificationFileContent") }); | ||
} | ||
} | ||
return autoResponse({ body: "Not found", status: 400 }) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.