-
Notifications
You must be signed in to change notification settings - Fork 0
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
Showing
8 changed files
with
487 additions
and
1 deletion.
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 |
---|---|---|
|
@@ -102,3 +102,7 @@ dist | |
|
||
# TernJS port file | ||
.tern-port | ||
|
||
config.json | ||
token.json | ||
notifications.json |
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 |
---|---|---|
@@ -1 +1,92 @@ | ||
# anilist-notifications | ||
<h1 align="center">anilist-notifications</h1> | ||
<p> | ||
<img src="https://img.shields.io/badge/node-18.x-blue.svg" /> | ||
<a href="https://github.com/emmaexe/anilist-notifications/wiki" target="_blank"> | ||
<img alt="Documentation" src="https://img.shields.io/badge/documentation-yes-brightgreen.svg" /> | ||
</a> | ||
<a href="https://github.com/emmaexe/anilist-notifications/graphs/commit-activity" target="_blank"> | ||
<img alt="Maintenance" src="https://img.shields.io/badge/Maintained%3F-yes-green.svg" /> | ||
</a> | ||
<a href="https://github.com/emmaexe/anilist-notifications/blob/main/LICENSE" target="_blank"> | ||
<img alt="License: GPL-3.0-ONLY" src="https://img.shields.io/github/license/emmaexe/anilist-notifications" /> | ||
</a> | ||
</p> | ||
|
||
> A notification server for Anilist that uses ntfy. | ||
### 🏠 [Repository](https://github.com/emmaexe/anilist-notifications) | ||
|
||
## **Dependencies** | ||
|
||
- NodeJS 18.x | ||
- node-fetch | ||
|
||
## **Installation** | ||
|
||
Download or clone the [latest release](https://github.com/emmaexe/anilist-notifications/releases/latest). | ||
|
||
Enter the directory and run: | ||
|
||
```sh | ||
npm install | ||
``` | ||
|
||
## **Setup/configuration** | ||
|
||
[Go to the anilist website and navigate to Settings -> Apps -> Developer.](https://anilist.co/settings/developer) Press on "Create New Client. Pick whatever you want as the Name and enter ``https://anilist.co/api/v2/oauth/pin`` as the Redirect URL. After the app is created, click on it and copy the number under the ID field. Ignore all other fields (Secret, Name, Redirect URL). The config-template folder in the root directory of the project contains the template for the configuration file. Create a folder called ``config`` (or just rename ``config-template`` to ``config``), copy the config template into it and rename it to `config.json` (from ``config.json.template``). Fill in the configuration file (this is where you paste the previously copied Api Client ID) and run the bot. | ||
|
||
Quick explanation of the config.json file: | ||
|
||
```json | ||
{ | ||
"ntfy": { | ||
"address": STRING, | ||
"port": INT, | ||
"topic": STRING, | ||
"authType": STRING, | ||
"username": STRING, | ||
"password": STRING | ||
}, | ||
"id": STRING, | ||
"refresh": INT | ||
} | ||
``` | ||
|
||
- `ntfy` contains settings regarding the ntfy server you are connecting to: | ||
- `address` is just the address of the server and nothing else (e.g. `ntfy.sh`). | ||
- `port` is the port where the ntfy server is hosted. | ||
- `topic` is the topic to which this server will send notifications to. Quoting the ntfy docs: "Topics are created on the fly by subscribing or publishing to them. Because there is no sign-up, the topic is essentially a password, so pick something that's not easily guessable." | ||
- `authType` is the auth type for the ntfy server. This is only useful if using a selfhosted ntfy server where you use credentials to log in. If you are using such a server you can also ignore the quote on topic names from the previous config entry. Possible auth types are `header` and `param`. Header sends the auth username+password as a header and param as a URL parameter. If the key is left empty, something random is typed in or the key is deleted, no authentication will be used | ||
- `username` ntfy username used in auth | ||
- `password` ntfy password used in auth | ||
- `id` is the Api Client ID you copied before. Make sure to enter it in the config as a string not an integer. | ||
- `refresh` is the delay between checking for notifications with the Anilist API (in miliseconds). The default is 60000 (that is 60 seconds, just divide by 1000) and the minimum is 15000 (15 seconds). If you try to go lower the delay will be increased to 15000 automatically to not spam the Anilist API. | ||
|
||
When starting the bot up for the first time after finishing the config file, you will be prompted to follow a URL and authorize the application you made earlier to access your account. You will then recieve a token you will need to paste into the console. This token grants the server 1 year of access to your account's API. You will recieve a notification via the same ntfy topic you use for Anilist notifications when the token gets close to expiry. The same topic will be used for notifications of any Anilist errors that may occur. Should something go wrong or your token is revoked and the app does not request you re-create the token, simply delete the `token.json` file in the root of the project to make the server forget the previous token and prompt you to set it up again. | ||
|
||
## **Usage** | ||
|
||
Run this command in the root directory of the cloned repository: | ||
|
||
```sh | ||
node . | ||
``` | ||
|
||
## **Author** | ||
|
||
👤 **Emmaexe** | ||
|
||
- Github: [@emmaexe](https://github.com/emmaexe) | ||
- Discord: [@emmaexe#0859](https://discord.gg/v4YrAgBRvz) | ||
|
||
## 🤝 Contributing | ||
|
||
Contributions, issues and feature requests are welcome!<br />Feel free to check [issues page](https://github.com/emmaexe/anilist-notifications/issues). | ||
|
||
## Show your support | ||
|
||
Give a ⭐️ if this project helped you! | ||
|
||
## 📝 License | ||
|
||
This project is [GPL-3.0-only](https://github.com/emmaexe/anilist-notifications/blob/main/LICENSE) licensed. |
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,12 @@ | ||
{ | ||
"ntfy": { | ||
"address": STRING, | ||
"port": INT, | ||
"topic": STRING, | ||
"authType": STRING, | ||
"username": STRING, | ||
"password": STRING | ||
}, | ||
"id": STRING, | ||
"refresh": INT | ||
} |
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,117 @@ | ||
const readline = require('readline') | ||
const configfile = require('./config/config.json') | ||
const fs = require('fs') | ||
const fetch = require('node-fetch'); | ||
const { notificationBuilder } = require('./ntfy.js') | ||
const query = `query { | ||
page: Page { | ||
notifications: notifications { | ||
... on AiringNotification { | ||
id | ||
type | ||
animeId | ||
episode | ||
contexts | ||
createdAt | ||
media: media { | ||
id | ||
title { userPreferred } | ||
type | ||
coverImage { color, medium, large, extraLarge } | ||
siteUrl | ||
} | ||
} | ||
} | ||
} | ||
}` | ||
|
||
module.exports = { | ||
"configClass": class config { | ||
constructor() { | ||
this.token = {} | ||
for (let item in configfile) { this[item] = configfile[item]; } | ||
if (!this.refresh) { | ||
this.refresh = 60000; | ||
} else if (this.refresh < 15000) { | ||
this.refresh = 15000; | ||
} | ||
this.token_access() | ||
} | ||
async token_access() { | ||
fs.readFile('./token.json', {encoding:'utf8'}, (err, data) => { | ||
if (err) { console.error(err); return this.token_constructor(); } else { | ||
let tokenfile = JSON.parse(data); | ||
if (tokenfile === undefined) { return this.token_constructor(); } else { | ||
if (!(tokenfile.token && tokenfile.expires)) { return this.token_constructor(); } else { | ||
this.token = { | ||
'token': tokenfile.token, | ||
'expires': tokenfile.expires | ||
} | ||
return; | ||
} | ||
} | ||
} | ||
}) | ||
} | ||
async token_constructor() { | ||
console.clear() | ||
const cin = readline.createInterface({ | ||
input: process.stdin, | ||
output: process.stdout | ||
}); | ||
cin.question(`Please follow this URL to grant the server an auth token.\nhttps://anilist.co/api/v2/oauth/authorize?client_id=${this.id}&response_type=token\nYou will need to copy paste it into the console.\nYou only need to do this once a year (the lifespan of the token)\n`, async token => { | ||
let newtokenfile = {'token': token, 'expires': Date.now() + 31536000000} | ||
fs.writeFile('./token.json', JSON.stringify(newtokenfile), (err) => { | ||
if (err) console.error(err); | ||
cin.close(); | ||
process.exit(); | ||
}) | ||
}); | ||
} | ||
async token_verify(manager) { | ||
let notification = new notificationBuilder() | ||
let url = 'https://graphql.anilist.co'; | ||
let options = { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json', | ||
'Accept': 'application/json', | ||
'Authorization': `Bearer ${this.token.token}` | ||
}, | ||
body: JSON.stringify({ | ||
'query': query | ||
}) | ||
}; | ||
let res_raw = await fetch(url, options); let res = await res_raw.json(); | ||
if (res['errors'] != undefined) { | ||
if (res.errors[0].message === "Invalid token" && res.errors[0].status === 400) { | ||
notification.setTitle("AniList token expired.") | ||
notification.setBody("Your AniList token has expired or is invalid. AniList tokens have a lifespan of 1 year. Please re-create the token to continue receiving your AniList push notifications.") | ||
notification.setIcon('https://anilist.co/img/icons/android-chrome-512x512.png') | ||
notification.setClickEvent("https://anilist.co/settings/apps") | ||
notification.setPriority('urgent') | ||
manager.send(notification) | ||
} else { | ||
notification.setTitle("Generic AniList API error.") | ||
notification.setBody(`Error ${res.errors[0].status}: ${res.errors[0].message}`) | ||
notification.setIcon('https://anilist.co/img/icons/android-chrome-512x512.png') | ||
notification.setClickEvent("https://anilist.co/settings/apps") | ||
notification.setPriority('high') | ||
manager.send(notification) | ||
} | ||
} else { | ||
let now = Date.now(); | ||
if ( (this.token.expires - now) <= 604800000 ) { | ||
let expiryDate = (new Date(this.token.expires)).toISOString() | ||
notification.setTitle("Your anilist token will expire within a week.") | ||
notification.setBody(`Your AniList token will expire on: ${expiryDate}. AniList tokens have a lifespan of 1 year. Please re-create the token to continue receiving your AniList push notifications.`) | ||
notification.setIcon('https://anilist.co/img/icons/android-chrome-512x512.png') | ||
notification.setClickEvent("https://anilist.co/settings/apps") | ||
notification.setPriority('default') | ||
manager.send(notification) | ||
} | ||
} | ||
} | ||
}, | ||
"query": query | ||
} |
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,109 @@ | ||
const http = require('http'); | ||
function httpsPost({body, ...options}) { | ||
return new Promise((resolve,reject) => { | ||
const req = http.request({ | ||
method: 'POST', | ||
...options, | ||
}, res => { | ||
const chunks = []; | ||
res.on('data', data => chunks.push(data)) | ||
res.on('end', () => { | ||
let resBody = Buffer.concat(chunks); | ||
switch(res.headers['content-type']) { | ||
case 'application/json': | ||
resBody = JSON.parse(resBody); | ||
break; | ||
} | ||
resolve(resBody) | ||
}) | ||
}) | ||
req.on('error',reject); | ||
if(body) { | ||
req.write(body); | ||
} | ||
req.end(); | ||
}) | ||
} | ||
|
||
module.exports = { | ||
"notificationManager": class notificationManager{ | ||
constructor(address, port, topic, authType, username, password) { | ||
this.address = address | ||
this.port = isNaN(port) ? 80 : port | ||
this.topic = topic | ||
this.username = username | ||
this.password = password | ||
this.authType = (authType === undefined) ? 'none' : ((authType === 'header') ? 'header' : ((authType === 'param') ? 'param' : undefined)) | ||
this.authHeader = Buffer.from(`${this.username}:${this.password}`).toString('base64') | ||
this.authParam = Buffer.from("Basic " + this.authHeader).toString('base64') | ||
} | ||
async send(notification) { | ||
let path; | ||
if (this.authType === 'none' || this.authType === undefined) { | ||
path = `/${this.topic}` | ||
} else if (this.authType === 'header') { | ||
path = `/${this.topic}` | ||
notification.headers.Authorization = `Basic ${this.authHeader}` | ||
} else if (this.authType === 'param') { | ||
path = `/${this.topic}?auth=${this.authParam}` | ||
} | ||
const res = await httpsPost({ | ||
hostname: this.address, | ||
port: this.port, | ||
path: path, | ||
headers: notification.headers, | ||
body: notification.body | ||
}) | ||
} | ||
}, | ||
"notificationBuilder": class notificationBuilder{ | ||
constructor() { | ||
this.headers = {} | ||
this.body = "" | ||
} | ||
setBody(body) { | ||
this.body = body | ||
} | ||
setTitle(title) { | ||
this.headers.Title = title | ||
} | ||
setPriority(priority) { | ||
if (priority == 5 || priority == 'max' || priority == 'urgent') { | ||
this.headers.Priority = 'urgent' | ||
} else if (priority == 4 || priority == 'high') { | ||
this.headers.Priority = 'high' | ||
} else if (priority == 3 || priority == 'default') { | ||
this.headers.Priority = 'default' | ||
} else if (priority == 2 || priority == 'low') { | ||
this.headers.Priority = 'low' | ||
} else if (priority == 1 || priority == 'min') { | ||
this.headers.Priority = 'min' | ||
} else { | ||
this.headers.Priority = 'default' | ||
} | ||
} | ||
addTags(tags) { | ||
if (Array.isArray(tags)) { | ||
if (tags.length > 0) { | ||
if (!this.headers.Tags) this.headers.Tags = "" | ||
for (let tag in tags) { | ||
if (this.header.Tags.length > 0) this.header.Tags+=',' | ||
this.headers.Tags+=tag.toString() | ||
} | ||
} | ||
} | ||
} | ||
scheduleDelivery(time) { | ||
this.headers.At = time | ||
} | ||
setClickEvent(url) { | ||
this.headers.Click = url | ||
} | ||
setAttachment(url) { | ||
this.headers.Attach = url | ||
} | ||
setIcon(url) { | ||
this.headers.Icon = url | ||
} | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.