first code commit

This commit is contained in:
Hugh Rundle 2021-05-15 15:03:59 +10:00
parent e80e633335
commit 3b7931d445
5 changed files with 265 additions and 1 deletions

View file

@ -1,2 +1,53 @@
# mastodon-clippy # mastodon-clippy
customisable clippy bot for mastodon A customisable nodejs clippy bot for mastodon.
`mastodon-clippy` notices when you are tooting about a topic that is bad for your health, and gently suggests you stop.
See an example in action at [auspol_clippy](https://ausglam.space/@auspol_clippy).
## configuration
`mastodon-clippy` takes all configuration as ENV variables:
* `CLIPPY_TOPIC` is the topic your clippy bot makes suggestions for. e.g. "auspol".
* `CLIPPY_DOMAIN` is the base domain for the Mastodon server your bot runs on _without a protocol_. e.g. "botsin.space"
* `CLIPPY_USER` is the username of the bot, e.g. "auspol_clippy".
* `CLIPPY_ACCESS_TOKEN` is the API access token for your bot.
## auto-config
Some settings for your bot account will be automatically set/overridden whenever the bot starts. These are:
```json
locked: false,
bot: true,
discoverable: true,
source: { privacy: 'private' }
```
That is, your bot must always:
* accept new followers
* declare it is a bot
* be discoverable on the server suggestions page
* post messages in "followers only" mode
## manual config
It does not appear to be possible to use the API to set accounts to hide their social graph. users should be able to use your bot without other people necessarily knowing, but the bot needs to follow them in order to work. Therefore you should manually select `Hide your network` in `https://example.com/settings/preferences/other`.
## setup
You can use the example systemd file at `mastodon-clippy.service.example` tweaked to suit your setup. This will keep the bot running and set your environment variables as above.
# running
Start the bot with the traditional `node index.js`.
## interacting with the bot
To "sign up" for notification from your bot, users have two options:
1. follow the bot account
2. send a toot to the bot with the word `START` in capital letters. e.g. `@auspol_clippy START`
To "unsubscribe" from the bot, users can send a toot with the word `STOP` in capital letters. e.g. `@auspol_clippy STOP`

156
index.js Normal file
View file

@ -0,0 +1,156 @@
/*
Clippy bot for mastodon.
(c) Hugh Rundle, licensed AGPL 3.0+
Contact @hugh@ausglam.space
NOTE: Since requesting users must be followed by the bot, they will "out" themselves by using it.
This does not appear to be fixable with the API, so bot owners should manually "Hide network".
*/
// require modules
const axios = require('axios')
const crypto = require('crypto') // built in node module requires v12.19 or higher
const WebSocket = require('ws')
// clippy settings
const access_token = process.env.CLIPPY_ACCESS_TOKEN
const topic = process.env.CLIPPY_TOPIC
const clippy = process.env.CLIPPY_USER
const domain = process.env.CLIPPY_DOMAIN
// set authorization headers for all API calls
const headers = {
'Authorization' : `Bearer ${access_token}`
}
// set up bot account with correct settings
function initiateSettings() {
let account = {
locked: false,
bot: true,
discoverable: true,
source: { privacy: 'private' }
}
// update with the above settings
return axios.patch(`https://${domain}/api/v1/accounts/update_credentials`, account, { headers: headers })
.catch( err => {
console.error('ERROR applying bot user settings: ', err.message)
})
}
function suggestion() {
const n = crypto.randomInt(4)
switch(n) {
case 0:
return 'How about logging off instead?';
case 1:
return 'Would you like to delete your toot?';
case 2:
return 'Can I help you take a walk outside?';
case 3:
return 'You may like to reconsider your life choices.';
}
}
// send a message when someone toots about the topic
function sendResponse(rip, user) {
let payload = {
'status' : `@${user} It looks like you're posting about '${topic}'. ${suggestion()}`,
'in_reply_to_id' : rip,
}
axios.post(`https://${domain}/api/v1/statuses`, payload, { headers: headers })
.catch( err => {
console.error(err.message)
})
}
function followAction(id, action) {
let url = `https://${domain}/api/v1/accounts/${id}/${action}`
let payload = {
reblogs: false,
notify: false
}
axios.post(url, payload, { headers: headers })
.catch( err => {
console.error(err.message)
})
}
// ***********************
// STREAMING USER TIMELINE
// This is where the action is!
// ***********************
const ws = new WebSocket(`wss://${domain}/api/v1/streaming?access_token=${access_token}&stream=user`)
// make sure bot is set up correctly each time it starts
initiateSettings()
// errors
ws.on('error', err => {
console.error(`WebSocket error: ${err.message}`)
})
// check updates and notifications in the stream
ws.on('message', msg => {
let packet = JSON.parse(msg)
let data = JSON.parse(packet.payload)
// notifications
if (packet.event == 'notification') {
// always follow back
if (data.type == 'follow') {
followAction(data.account.id, 'follow')
}
if (data.type == 'mention') {
let post = data.status.content
// check start requests
if (post.match(/\bSTART\b/)) {
followAction(data.account.id, 'follow')
}
// check stop requests
if (post.match(/\STOP\b/)) {
followAction(data.account.id, 'unfollow')
}
}
}
// updates (posts)
if (packet.event == 'update') {
let rip = data.id
let user = data.account.username
// exclude own toots to avoid an infinite loop
if (data.account.username !== clippy) {
if ( data.content.includes(topic) ) {
sendResponse(rip, user)
}
else if (data.spoiler_text == topic) {
sendResponse(rip, user)
}
else if (data.tags.includes(topic)) {
sendResponse(rip, user)
}
}
}
})

View file

@ -0,0 +1,15 @@
[Service]
Type=simple
ExecStart=/usr/bin/node index.js
Restart=always
RestartSec=15
TimeoutSec=15
KillMode=process
User=clippy
WorkingDirectory=/home/clippy/mastodon-clippy
Environment="CLIPPY_TOPIC=auspol"
Environment="CLIPPY_ACCESS_TOKEN=your-token-here"
Environment="CLIPPY_DOMAIN=example.com"
Environment="CLIPPY_USER=auspol_clippy"
[Install]
WantedBy=multi-user.target

26
package-lock.json generated Normal file
View file

@ -0,0 +1,26 @@
{
"name": "mastodon-clippy",
"version": "0.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"axios": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
"requires": {
"follow-redirects": "^1.10.0"
}
},
"follow-redirects": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz",
"integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg=="
},
"ws": {
"version": "7.4.5",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz",
"integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g=="
}
}
}

16
package.json Normal file
View file

@ -0,0 +1,16 @@
{
"name": "mastodon-clippy",
"version": "1.0.0",
"description": "Mastodon clippy bot",
"repository": "https://github.com/hughrun/mastodon-clippy.git",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Hugh Rundle <hugh@hughrundle.net> (https://www.hughrundle.net)",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",
"ws": "^7.4.5"
}
}