Initial commit
This commit is contained in:
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal 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
11
Dockerfile
Normal 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
21
LICENSE
Normal 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.
|
||||||
51
app.js
Normal file
51
app.js
Normal 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
BIN
namecard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
33
package.json
Normal file
33
package.json
Normal 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
17
patchnote-publisher.js
Normal 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
21
patchnote.md
Normal 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
11
renovate.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:recommended",
|
||||||
|
":disableDependencyDashboard",
|
||||||
|
":preserveSemverRanges"
|
||||||
|
],
|
||||||
|
"ignorePaths": [
|
||||||
|
"**/node_modules/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
6
tests/name-card-test.js
Normal file
6
tests/name-card-test.js
Normal 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
8
tests/polling.js
Normal 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
31
tests/token-renew.js
Normal 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
5
tests/token-watch.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import {data} from "../wwwroot/core/appData.js";
|
||||||
|
|
||||||
|
|
||||||
|
data.instagramTokenManager.watchDelay = 5*1000;
|
||||||
|
data.instagramTokenManager.startWatching();
|
||||||
BIN
wwwroot/assets/avatar-test.png
Normal file
BIN
wwwroot/assets/avatar-test.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 764 KiB |
BIN
wwwroot/assets/name-card-template.png
Normal file
BIN
wwwroot/assets/name-card-template.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 946 KiB |
52
wwwroot/core/appData.js
Normal file
52
wwwroot/core/appData.js
Normal 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 : ""}
|
||||||
|
);
|
||||||
|
*/
|
||||||
20
wwwroot/core/base/MessageSender.js
Normal file
20
wwwroot/core/base/MessageSender.js
Normal 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!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
32
wwwroot/core/base/basePoller.js
Normal file
32
wwwroot/core/base/basePoller.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
wwwroot/core/base/baseTokenManager.js
Normal file
47
wwwroot/core/base/baseTokenManager.js
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
53
wwwroot/core/instagram/instagramPoller.js
Normal file
53
wwwroot/core/instagram/instagramPoller.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
88
wwwroot/core/instagram/instagramTokenManager.js
Normal file
88
wwwroot/core/instagram/instagramTokenManager.js
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
38
wwwroot/core/logging/logger.js
Normal file
38
wwwroot/core/logging/logger.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
wwwroot/core/tiktok/tikTokPoller.js
Normal file
26
wwwroot/core/tiktok/tikTokPoller.js
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
38
wwwroot/core/tiktok/tiktokTokenManager.js
Normal file
38
wwwroot/core/tiktok/tiktokTokenManager.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
26
wwwroot/core/usersToken.js
Normal file
26
wwwroot/core/usersToken.js
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
84
wwwroot/core/utils/jsonManager.js
Normal file
84
wwwroot/core/utils/jsonManager.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
35
wwwroot/core/utils/requester.js
Normal file
35
wwwroot/core/utils/requester.js
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
20
wwwroot/core/utils/stringFormatter.js
Normal file
20
wwwroot/core/utils/stringFormatter.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
78
wwwroot/core/welcome/nameCardCreator.js
Normal file
78
wwwroot/core/welcome/nameCardCreator.js
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
67
wwwroot/legal/privacy/content.html
Normal file
67
wwwroot/legal/privacy/content.html
Normal 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>
|
||||||
103
wwwroot/legal/terms-of-service/content.html
Normal file
103
wwwroot/legal/terms-of-service/content.html
Normal 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
26
wwwroot/oauth/index.html
Normal 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>
|
||||||
Reference in New Issue
Block a user