Initial commit

This commit is contained in:
2025-11-09 10:23:23 +01:00
commit f890341ffe
34 changed files with 1058 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
.idea
logs
node_modules
package-lock.json
.env
.instagram_client_tokens.json
.tiktok_client_tokens.json
posts.json

11
Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM node:latest
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["node", "app.js"]

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Naaturel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1
README.md Normal file
View File

@@ -0,0 +1 @@
Nothing here for now

51
app.js Normal file
View File

@@ -0,0 +1,51 @@
import { Logger } from './wwwroot/core/logging/logger.js';
import JsonManager from './wwwroot/core/utils/jsonManager.js';
import { data } from "./wwwroot/core/appData.js";
try{
data.instagramTokenManager.startWatching();
//data.tiktokTokenManager.startWatching();
data.instagramPoller.on("newPost", async ({permalink, userId}) => {
const modified = JsonManager.upsertPost("./posts.json", {permalink, userId})
if(!modified) return;
const message = `Behold ! Some new awesome content has been posted ! \n\n${permalink}`;
await data.sender.send(data.socialChannelID, message)
});
data.client.once('clientReady', async () => {
await data.sender.send(data.updateChannelID, "I'm now online ! ✅")
console.log(`✅ Logged in as ${data.client.user.tag}`);
});
data.client.on('messageCreate', (message) => {
const isGuildOwner = message.guild && message.author.id === message.guild.ownerId;
if (message.content === '/login' && isGuildOwner) {
message.reply(data.instagramTokenManager.getOauthUrl());
}
});
data.client.on('guildMemberAdd', member => {
const avatar = member.user.avatarURL();
});
process.on('SIGINT', async () => {
try {
await data.sender.send(data.updateChannelID, "I'm shutting down, now. Bye! 👋")
} catch (error) {
await Logger.error("Error while shutting down", error);
} finally {
await data.client.destroy();
process.exit(0);
}
});
await data.client.login(process.env.DISCORD_TOKEN);
data.instagramPoller.start(600000);
} catch(err){
await Logger.error("Unexpected error", err)
}

BIN
namecard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

33
package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "the-jailor",
"private": true,
"version": "1.0.0",
"description": "A simple discord bot",
"main": "app.js",
"type": "module",
"engines": {
"node": ">=18.x"
},
"scripts": {
"start": "node app.js",
"register": "node commands.js",
"dev": "nodemon app.js",
"publish-patch-note" : "node patchnote-publisher.js",
"name-card-test": "node tests/name-card-test.js",
"istg-token-test": "node tests/token-renew.js",
"token-watch-test" : "node tests/token-watch.js",
"polling-test" : "node tests/polling.js"
},
"author": "Naaturel",
"license": "MIT",
"dependencies": {
"discord.js": "^14.22.1",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"prompt-sync": "^4.2.0",
"sharp": "^0.34.5"
},
"devDependencies": {
"nodemon": "^3.0.0"
}
}

17
patchnote-publisher.js Normal file
View File

@@ -0,0 +1,17 @@
import { data } from "./wwwroot/core/appData.js";
import path from "path";
import {readFileSync} from "fs";
import {Logger} from "./wwwroot/core/logging/logger.js";
try{
const filePath = "./patchnote.md";
const fullPath = path.resolve(filePath);
const patchNoteContent = readFileSync(fullPath, 'utf8');
await data.client.login(process.env.DISCORD_TOKEN);
await data.sender.send(data.updateChannelID, patchNoteContent);
await data.client.destroy();
} catch (err){
await Logger.log("An error occurred while publishing patch note", err);
}

21
patchnote.md Normal file
View File

@@ -0,0 +1,21 @@
# What's new?
## Patch notes
> The jailor will now send a patch note every time a new feature is added.
## Logging in
> You can grant permission to the jailor by typing /login command. A link will be sent, click it, follow the instructions and... done!
> NB : This command can be only be used by guild owner
## Refreshing logging
> Granted permissions are not eternal, the jailor will now ask Instagram and TikTok APIs for new tokens as soon as needed. No need to manually re-log in anymore
# What's next?
## Customized name card
> A customized name card will be generated and sent in a specific channel when a new user join the guild!
## Tiktok login
> You will soon be able to log in with TikTok to post updates about you content
# Questions?
> Ask me on Twitter! https://twitter.com/naaturel_

11
renovate.json Normal file
View File

@@ -0,0 +1,11 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":disableDependencyDashboard",
":preserveSemverRanges"
],
"ignorePaths": [
"**/node_modules/**"
]
}

0
test.json Normal file
View File

6
tests/name-card-test.js Normal file
View File

@@ -0,0 +1,6 @@
import {NameCardCreator} from "../wwwroot/core/welcome/nameCardCreator.js";
const templatePath = "./assets/name-card-template.png";
const avatarPath = "./assets/avatar-test.png";
const res = await NameCardCreator.getWelcomeCard(templatePath, avatarPath);

8
tests/polling.js Normal file
View File

@@ -0,0 +1,8 @@
import { data } from "../wwwroot/core/appData.js";
try {
console.log("Testing polling...");
await data.instagramPoller.pollOnce();
} catch(e){
console.error(e);
}

31
tests/token-renew.js Normal file
View File

@@ -0,0 +1,31 @@
import {data} from '../wwwroot/core/appData.js';
import promptSync from 'prompt-sync';
const userData = await generate();
console.log(userData);
const newUserData = await renew(userData)
console.log(newUserData);
async function generate(){
console.log("Testing token generation...")
const url = data.instagramTokenManager.getOauthUrl();
console.log("Go to this url : \n" + url);
const prompt = promptSync();
const code = prompt('Enter generated code : ');
return await data.instagramTokenManager.generate(code)
}
async function renew(userData){
console.log("Testing token refresh...")
const res = await data.instagramTokenManager.renew(userData);
console.log(res)
}

5
tests/token-watch.js Normal file
View File

@@ -0,0 +1,5 @@
import {data} from "../wwwroot/core/appData.js";
data.instagramTokenManager.watchDelay = 5*1000;
data.instagramTokenManager.startWatching();

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 946 KiB

52
wwwroot/core/appData.js Normal file
View File

@@ -0,0 +1,52 @@
import 'dotenv/config';
import {Client, GatewayIntentBits} from "discord.js";
import {MessageSender} from "./base/MessageSender.js";
import {InstagramTokenManager} from "./instagram/instagramTokenManager.js";
import {TikTokTokenManager} from "./tiktok/tiktokTokenManager.js";
import JsonManager from "./utils/jsonManager.js";
import {InstagramPoller} from "./instagram/instagramPoller.js";
import {UsersToken} from "./usersToken.js";
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMembers,
]
});
const sender = new MessageSender(client);
const instagramID = process.env.INSTAGRAM_CLIENT_ID;
const instagramSecret = process.env.INSTAGRAM_CLIENT_SECRET;
const updateChannelID = process.env.BOT_UPDATE_CHANNEL_ID;
const socialChannelID = process.env.SOCIAL_CHANNEL_ID
const filePath = ".instagram_client_tokens.json";
const usersToken = new UsersToken(filePath)
const instagramTokenManager = new InstagramTokenManager(
{clientID : instagramID, clientSecret : instagramSecret, usersToken : usersToken}
);
const instagramPoller = new InstagramPoller(process.env.INSTAGRAM_POST_LIST, process.env.INSTAGRAM_POST_DETAILS, instagramTokenManager);
export const data = {
client,
sender,
instagramPoller,
updateChannelID,
socialChannelID,
instagramTokenManager,
//tiktokTokenManager
};
/*
const tiktokTokenManager = new TikTokTokenManager(
{filepath : ".tiktok_client_tokens.json", clientKey : "", clientSecret : ""}
);
*/

View File

@@ -0,0 +1,20 @@
import {data} from "../appData.js";
export class MessageSender {
constructor(client) {
this.client = client;
}
async send(channelID, message) {
const channel = await data.client.channels.fetch(channelID);
if (channel && channel.isTextBased()) {
await channel.send(message);
} else {
console.log('❌ Channel not found!');
}
}
}

View File

@@ -0,0 +1,32 @@
import EventEmitter from "events";
import {Logger} from "../logging/logger.js";
export class BasePoller extends EventEmitter {
constructor() {
super();
this.task = null;
}
async pollOnce(){
throw new Error("Not implemented.");
}
start(interval) {
if(this.task){
Logger.error("A task has already been scheduled");
return;
}
super.task = setInterval(async () => {
await this.pollOnce();
}, interval);
}
stop() {
if (this.task) {
clearInterval(this.task);
super.task = null;
}
}
}

View File

@@ -0,0 +1,47 @@
export class BaseTokenManager {
/**
*
* @param usersToken {UsersToken}
*/
constructor(usersToken) {
this.usersToken = usersToken;
this.watchDelay = 3600*1000; //1 hour in milliseconds
}
/**
* Launches watch for all users
*/
startWatching() {
this.usersToken.getAll().forEach((user, index) => {
this.watchUser(index);
})
}
/**
* Watches token for a single user
*/
watchUser(userIndex){
setInterval(async () => {
let userData = await this.getUserData(userIndex);
if(this.mustBeRenewed(userData.expiresAt)){
await this.renew(userData);
}
}, this.watchDelay);
}
renew(){
throw new Error("Not implemented.");
}
mustBeRenewed(expiresAt) {
const expirationThreshold = 3600*1000; //1 hour in milliseconds
const timeBeforeExpiration = expiresAt - Date.now();
return timeBeforeExpiration <= expirationThreshold;
}
getUserData(index){
return this.usersToken.get(index);
}
}

View File

@@ -0,0 +1,53 @@
import { Logger } from '../logging/logger.js';
import StringFormatter from '../utils/stringFormatter.js';
import {BasePoller} from "../base/basePoller.js";
import {Requester} from "../utils/requester.js";
export class InstagramPoller extends BasePoller {
/**
*
* @param rawPostListUrl {string}
* @param rawMediaDetailsUrl {string}
* @param tokenManager {InstagramTokenManager}
*/
constructor(rawPostListUrl, rawMediaDetailsUrl, tokenManager){
super();
this.rawPostListUrl = rawPostListUrl;
this.rawMediaDetailsUrl = rawMediaDetailsUrl;
this.tokenManager = tokenManager;
}
async pollOnce(){
try{
let userData = this.tokenManager .getUserData(0);
let postListUrl = this.formatPostListUrl(userData.userId, userData.token);
let mediaData = await Requester.doGetRequest(postListUrl);
console.log(mediaData);
let mediaId = this.getMediaId(mediaData);
let detailsUrl = this.formatMediaDetailsUrl(mediaId, userData.token);
let mediaDetails = await Requester.doGetRequest(detailsUrl);
console.log(mediaDetails);
console.log({permalink : mediaDetails.permalink, userId : userData.userId});
super.emit("newPost", {permalink : mediaDetails.permalink, userId : userData.userId});
} catch(err) {
console.error(err);
await Logger.error(`Unbale to fetch instagram content for`, err)
}
}
formatPostListUrl(userId, accessToken){
return StringFormatter.format(this.rawPostListUrl, [{key: "userId", value: userId}, {key: "access_token", value: accessToken}]);
}
formatMediaDetailsUrl(mediaId, accessToken){
return StringFormatter.format(this.rawMediaDetailsUrl, [{key: "access_token", value: accessToken}, {key: "media_id", value: mediaId}]);
}
getMediaId(mediaData){
return mediaData.data[0].id;
}
}

View File

@@ -0,0 +1,88 @@
import {BaseTokenManager} from "../base/baseTokenManager.js";
import {Requester} from "../utils/requester.js";
export class InstagramTokenManager extends BaseTokenManager {
/**
*
* @param clientID {string}
* @param clientSecret {string}
* @param usersToken {UsersToken}
*/
constructor({clientID, clientSecret, usersToken}) {
super(usersToken);
this.clientID = clientID;
this.clientSecret = clientSecret;
}
getOauthUrl(){
return "https://www.instagram.com/oauth/authorize"
+ `?client_id=${this.clientID}`
+ "&redirect_uri=https://the-jailor.naaturel.be/oauth/"
+ "&scope=instagram_business_basic,instagram_business_manage_messages,instagram_business_manage_comments,"
+ "instagram_business_content_publish,instagram_business_manage_insights"
+ "&response_type=code";
}
async generate(code) {
let headers = {
"Content-Type": "application/x-www-form-urlencoded"
};
let body = {
"client_id": this.clientID,
"client_secret": this.clientSecret,
"grant_type": "authorization_code",
"redirect_uri": "https://the-jailor.naaturel.be/oauth/",
"code": code
};
let encodedBody = new URLSearchParams(body);
let shortLiveAccessTokenData = await Requester.doPostRequest(
"https://api.instagram.com/oauth/access_token", headers, encodedBody);
let userId = shortLiveAccessTokenData.user_id;
let shortLiveAccessToken = shortLiveAccessTokenData.access_token;
let longLiveAccessTokenData = await Requester.doGetRequest(
"https://graph.instagram.com/access_token?grant_type=ig_exchange_token" +
`&client_secret=${this.clientSecret}` +
`&access_token=${shortLiveAccessToken}`);
let accessToken = longLiveAccessTokenData.access_token;
let expiresAt = Date.now() + longLiveAccessTokenData.expires_in*1000; //convert from sec to millis
const result = {
userId : userId,
token : accessToken,
expiresAt : expiresAt
};
this.usersToken.upsert(result);
return result;
}
async renew(userData){
const data = await Requester.doGetRequest(
"https://graph.instagram.com/refresh_access_token" +
"?grant_type=ig_refresh_token" +
`&access_token=${userData.token}`
);
let accessToken = data.access_token;
let expiresAt = Date.now() + data.expires_in*1000; //convert from sec to millis
const result = {
userId : userData.userId,
token : accessToken,
expiresAt : expiresAt
}
this.usersToken.upsert(result);
return result;
}
}

View File

@@ -0,0 +1,38 @@
import path from "path";
import { appendFile, mkdir } from "fs/promises";
export class Logger{
static log(message){
console.log(message)
}
static async error(message, error) {
try {
const currentDateTime = new Date();
const date = currentDateTime.toLocaleDateString("fr-FR", {
day: "2-digit",
month: "2-digit",
year: "2-digit"
}).replace(/\//g, "-");
const time = currentDateTime.toLocaleTimeString("fr-FR", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false
});
const logsDir = path.resolve("./logs");
const fullPath = path.join(logsDir, `${date}.error.log`);
await mkdir(logsDir, { recursive: true });
await appendFile(fullPath, `${time} - ${message} -> ${JSON.stringify(error)} \n`);
console.error(`An error occured. The incident has been logged in ${fullPath}`)
} catch (err) {
console.error("Error writing log:", err);
}
}
}

View File

@@ -0,0 +1,26 @@
import {BasePoller} from "../base/basePoller.js";
import {Logger} from "../logging/logger.js";
export class TiktokPoller extends BasePoller {
constructor({userToken : userToken, endpoint : endpoint}) {
super();
this.endpoint = endpoint;
this.userToken = userToken;
}
async pollOnce() {
try {
//{
// 'Content-Type': 'application/json',
// 'Authorization': 'Bearer ' + this.userToken,
//}
//let data = super.doPostRequest(this.endpoint, this.userToken, {"max_count" : 1});
} catch (err) {
Logger.error("Unbale to fetch TikTok content", err)
}
}
}

View File

@@ -0,0 +1,38 @@
import {BaseTokenManager} from "../base/baseTokenManager.js";
import {Requester} from "../utils/requester.js";
export class TikTokTokenManager extends BaseTokenManager {
constructor({filepath, clientKey, clientSecret}) {
super(filepath);
this.clientKey = clientKey;
this.clientSecret = clientSecret;
}
async renew() {
let url = `https://www.tiktok.com/v2/auth/authorize?client_key=${this.clientKey}&response_type=code&scope=user.info.basic,video.list&redirect_uri=https://the-jailor.naaturel.be/oauth&state=SOME_RANDOM_STRING`
let response = await Requester.doGetRequest(url);
let userCode = response.code; //Might not be correct, please update
url = "https://open.tiktokapis.com/v2/oauth/token/";
let headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Cache-Control": "no-cache"
}
let params = {
"client_key": this.clientKey,
"client_secret": this.clientSecret,
"code": userCode,
"grant_type": "authorization_code",
"redirect_uri": "https://the-jailor.naaturel.be/oauth"
}
let data = await Requester.doPostRequest(url, headers, params);
let userId = data.open_id;
let token = data.access_token;
//Update token registry
}
}

View File

@@ -0,0 +1,26 @@
import JsonManager from "./utils/jsonManager.js";
export class UsersToken {
constructor(filePath) {
this.filePath = filePath;
this.data = JsonManager.read(this.filePath);
}
reload() {
this.data = JsonManager.read(this.filePath);
}
upsert(data) {
JsonManager.upsertToken(this.filePath, data);
this.reload();
}
get(index){
return this.data[index];
}
getAll(){
return this.data;
}
}

View File

@@ -0,0 +1,84 @@
import path from 'path';
import {readFileSync, writeFileSync} from 'fs';
import {Logger} from '../logging/logger.js';
export default class JsonManager {
/**
* Read the current data in a file
* @param {*} filePath
* @returns deserialized content
*/
static read(filePath) {
try {
const fullPath = path.resolve(filePath);
const jsonString = readFileSync(fullPath, 'utf8');
return JSON.parse(jsonString);
} catch (err) {
Logger.error('Error reading JSON file: ', err.message);
return null;
}
}
/**
* Write the given data in a file by overriding the previous data
* @param {*} filePath
* @param {*} data
* @returns
*/
static writeUnsafe(filePath, data) {
try {
const fullPath = path.resolve(filePath);
const jsonString = JSON.stringify(data);
writeFileSync(fullPath, jsonString, 'utf8', async err => {
if(err) await Logger.error(err.message);
});
} catch (err) {
Logger.error('Error writting in JSON file: ', err.message);
return null;
}
}
static upsertToken(filePath, userData) {
const existingData = JsonManager.read(filePath);
const index = existingData.findIndex(e => e.userId === userData.userId);
if (index !== -1) {
existingData.splice(index, 1, userData);
} else {
existingData.push(userData);
}
JsonManager.writeUnsafe(filePath, existingData);
return existingData;
}
static upsertPost(filePath, { userId, permalink }) {
const data = JsonManager.read(filePath);
let modified = false;
const index = data.findIndex(e => e.userId === userId);
if (index !== -1) {
if (data[index].permalink !== permalink) {
data[index].permalink = permalink;
modified = true;
}
} else {
data.push({ userId, permalink });
modified = true;
}
if (modified) {
JsonManager.writeUnsafe(filePath, data);
}
return modified;
}
}

View File

@@ -0,0 +1,35 @@
import {Logger} from "../logging/logger.js";
export class Requester {
static async doGetRequest(url){
return fetch(url)
.then(async response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status} ${response.statusText} : ${await response.text()}`);
}
return response.json();
})
.then(data => {
return data;
})
.catch(async error => {
await Logger.error(`Failed to fetch`, error);
throw error;
});
}
static async doPostRequest(url, headers, body){
return await fetch(url,{
method: 'POST',
headers: headers,
body: body
})
.then(response => response.json())
.then(data => {
return data;
})
.catch(error => {
Logger.error(`Unable to fetch`, error);
});
}
}

View File

@@ -0,0 +1,20 @@
export default class StringFormatter{
/**
* Replaces tokens in a string with their corresponding values.
*
* Each token in the string should match a key in the `values` array.
* For example, given `s = "Hi :name !"` and `values = [{ key: ":name", value: "Foo" }]`,
* the function will return "Hi Foo !".
*
* @param {string} s - The string containing tokens to replace.
* @param {Array<{key: string, value: string}>} values - An array of objects mapping each token (`key`) to its replacement (`value`).
* @returns {string} The string with tokens replaced by their corresponding values.
*/
static format(s, values) {
values.forEach(v => {
s = s.replace(`:${v.key}`, v.value);
});
return s;
}
}

View File

@@ -0,0 +1,78 @@
import sharp from "sharp";
import {Logger} from "../logging/logger.js";
export class NameCardCreator {
/**
* Combines a template image with a user avatar and saves it
* @param {string} templatePath - path to the template image
* @param {string} avatarPath - path to the avatar
* @returns {Promise<sharp.OutputInfo>} resulting image buffer
*/
static async getWelcomeCard(templatePath, avatarPath) {
try{
const template = await this.loadTemplate(templatePath);
const avatar = await this.handleAvatar(avatarPath);
const result = await template
.composite([{
input: avatar,
top: 215,
left: 200,
}])
.toFile("namecard.png")
console.log("✅ Welcome card created: welcome-card.png");
return result;
} catch(err) {
await Logger.error("Unable to create name card", err);
}
}
/**
* Loads the template file into a sharp instance
* @param {string} path - file path
* @returns {sharp.Sharp} sharp image object
*/
static async loadTemplate(path) {
return sharp(path);
}
/**
* Overlays an image (item) on top of a base image
* @param {sharp.Sharp|string} base - sharp image or path
* @param {{item:string, x:number, y:number}} overlay - overlay info
* @returns {Promise<sharp.Sharp>} combined image
*/
static async mergeBitmaps(base, { item, x, y }) {
return null;
}
static async handleAvatar(avatarPath) {
const avatarSize = 670;
const borderSize = 8;
const radius = avatarSize / 2;
const totalSize = avatarSize + borderSize * 2;
const avatarBuffer = await sharp(avatarPath)
.resize(avatarSize, avatarSize, { fit: "cover" })
.png()
.toBuffer();
return await sharp({
create: {
width: totalSize,
height: totalSize,
channels: 4,
background: "#0000"
}
})
.composite([
{ input: avatarBuffer, top: borderSize, left: borderSize }
])
.png()
.toBuffer();
}
}

View File

@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Privacy notice for Instagram and TikTok API integrations." />
<title>Privacy Notice | The Jailor</title>
<style>
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #fafafa;
color: #333;
margin: 0;
padding: 2rem;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.privacy-notice {
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
max-width: 600px;
padding: 2rem;
text-align: left;
}
h1 {
font-size: 1.5rem;
margin-bottom: 1rem;
color: #222;
}
p {
font-size: 1rem;
line-height: 1.6;
margin-bottom: 1rem;
}
a {
color: #007bff;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
footer {
font-size: 0.9rem;
color: #777;
margin-top: 1.5rem;
}
</style>
</head>
<body>
<main class="privacy-notice" aria-labelledby="privacy-title" role="region">
<h1 id="privacy-title">Privacy Notice</h1>
<p>
This app connects to Instagram and/or TikTok APIs to display or manage your social content with your permission.
We only access basic profile information and media data that you authorize.
This data is used solely to provide the features you request.
</p>
<p>
We do not share personal data with third parties for marketing or analytics purposes.
You can revoke access at any time through your Instagram or TikTok account settings.
</p>
<footer>© 2025 The Jailor. All rights reserved.</footer>
</main>
</body>
</html>

View File

@@ -0,0 +1,103 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Terms of Service for Social App using Instagram and TikTok APIs." />
<title>Terms of Service | The Jailor</title>
<style>
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #fafafa;
color: #333;
margin: 0;
padding: 2rem;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.privacy-notice {
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
max-width: 600px;
padding: 2rem;
text-align: left;
}
h1 {
font-size: 1.5rem;
margin-bottom: 1rem;
color: #222;
}
p {
font-size: 1rem;
line-height: 1.6;
margin-bottom: 1rem;
}
a {
color: #007bff;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
footer {
font-size: 0.9rem;
color: #777;
margin-top: 1.5rem;
}
</style>
</head>
<body>
<main class="terms" aria-labelledby="terms-title" role="region">
<h1 id="terms-title">Terms of Service</h1>
<p>
By using this app, you agree to these Terms of Service and our
<a href="https://the-jailor.naaturel.be/privacy" target="_blank" rel="noopener noreferrer">Privacy Policy</a>.
Please read them carefully before using our features.
</p>
<h2>1. Use of Service</h2>
<p>
This app allows you to connect your Instagram and/or TikTok accounts to access and manage your content with your consent.
You agree to use the service only as permitted by law and in accordance with the respective platform policies.
</p>
<h2>2. User Data and Permissions</h2>
<p>
We only access data that you explicitly authorize through the Instagram or TikTok APIs.
This includes limited profile and media data necessary to deliver the requested functionality.
You can revoke this access at any time in your social platform account settings.
</p>
<h2>3. Restrictions</h2>
<p>
You may not use this service to infringe on the rights of others, engage in unauthorized scraping,
distribute spam, or violate any laws or platform terms.
</p>
<h2>4. Service Availability</h2>
<p>
We may modify, suspend, or discontinue certain features at any time.
We are not liable for downtime or disruptions caused by third-party API changes or technical issues.
</p>
<h2>5. Limitation of Liability</h2>
<p>
To the maximum extent permitted by law, we are not responsible for any indirect, incidental, or consequential damages arising from your use of the service.
</p>
<h2>6. Changes to Terms</h2>
<p>
We may update these Terms from time to time. Continued use of the app after updates means you accept the revised terms.
</p>
<footer>
© 2025 The Jailor. All rights reserved. |
</footer>
</main>
</body>
</html>

26
wwwroot/oauth/index.html Normal file
View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>The Jailor auth</title>
</head>
<body>
<div id="result-text"></div>
</body>
</html>
<script type="module">
import {data} from "../core/appData.js";
try{
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get("code");
await data.instagramTokenManager.generate(code)
document.querySelector("#code").innerText = "Authentication successful. You can close this tab";
} catch(err){
document.querySelector("#code").innerText = "Authentication failed.";
}
</script>