Compare commits

..

No commits in common. "main" and "v1.0.1" have entirely different histories.
main ... v1.0.1

4 changed files with 69 additions and 127 deletions

View file

@ -45,14 +45,6 @@ Then run `npm install .` to install npm modules `axios` and `ws`.
Start the bot with the traditional `node index.js`. Start the bot with the traditional `node index.js`.
However, you probably want this to run automatically on a web server. You can do this with `systemd` using the example unit file: `mastodon-clippy.service.example`. Adjust this for your user and paths, and then activate it:
```bash
sudo cp mastodon-clippy.service.example /etc/systemd/system/mastodon-clippy.service
sudo systemctl enable mastodon-clippy.service
sudo systemctl start mastodon-clippy service
```
## interacting with the bot ## interacting with the bot
To "sign up" for notification from your bot, users have two options: To "sign up" for notification from your bot, users have two options:
@ -60,4 +52,4 @@ To "sign up" for notification from your bot, users have two options:
1. follow the bot account 1. follow the bot account
2. send a toot to the bot with the word `START` in capital letters. e.g. `@auspol_clippy START` 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` 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
View file

@ -1,4 +1,4 @@
/* /*
Clippy bot for mastodon. Clippy bot for mastodon.
@ -17,7 +17,7 @@ const WebSocket = require('ws')
// clippy settings // clippy settings
const access_token = process.env.CLIPPY_ACCESS_TOKEN const access_token = process.env.CLIPPY_ACCESS_TOKEN
const topic = process.env.CLIPPY_TOPIC.toLowerCase() const topic = process.env.CLIPPY_TOPIC
const clippy = process.env.CLIPPY_USER const clippy = process.env.CLIPPY_USER
const domain = process.env.CLIPPY_DOMAIN const domain = process.env.CLIPPY_DOMAIN
@ -26,14 +26,8 @@ const headers = {
'Authorization' : `Bearer ${access_token}` 'Authorization' : `Bearer ${access_token}`
} }
function resetConnection(socket) {
terminate(socket)
console.log(`waiting after error`)
setTimeout( function() { listen() }, 5000)
}
// set up bot account with correct settings // set up bot account with correct settings
function initiateSettings(socket) { function initiateSettings() {
let account = { let account = {
locked: false, locked: false,
@ -46,50 +40,31 @@ function initiateSettings(socket) {
return axios.patch(`https://${domain}/api/v1/accounts/update_credentials`, account, { headers: headers }) return axios.patch(`https://${domain}/api/v1/accounts/update_credentials`, account, { headers: headers })
.catch( err => { .catch( err => {
console.error('ERROR applying bot user settings: ', err.message) console.error('ERROR applying bot user settings: ', err.message)
terminate(socket)
}) })
} }
// return random suggestion string function suggestion() {
function suggestion(username) {
const n = crypto.randomInt(12) const n = crypto.randomInt(4)
switch(n) { switch(n) {
case 0: case 0:
return 'How about logging off instead?'; return 'How about logging off instead?';
case 1: case 1:
return `Would you like to delete your toot, @${username}?`; return 'Would you like to delete your toot?';
case 2: case 2:
return 'Can I help you take a walk outside? 🚶‍➡️'; return 'Can I help you take a walk outside?';
case 3: case 3:
return 'You may like to reconsider your life choices.'; return 'You may like to reconsider your life choices.';
case 4:
return 'Why not try looking at #CatsOfMastodon instead?';
case 5:
return `Come on @${username}, we've talked about this. 🤷‍♂️`;
case 6:
return `You should go look at some trees. Trees are calming 🌳`;
case 7:
return `I'm not angry. I'm just very disappointed. 😔`;
case 8:
return `You said you were going to stop doing that ...and yet here we are.`;
case 9:
return `Time to touch some grass 🌱`;
case 10:
return `Why not have a nice cup of tea instead? 🫖`;
case 11:
return `And yet you still haven't read all of those books in your TBR pile. 🤔`;
} }
} }
// send a message when someone toots about the topic // send a message when someone toots about the topic
function sendResponse(rid, user, username) { function sendResponse(rip, user) {
let payload = { let payload = {
'status' : `@${user} It looks like you're posting about '${topic}'. ${suggestion(username)}`, 'status' : `@${user} It looks like you're posting about '${topic}'. ${suggestion()}`,
'spoiler_text' : topic, 'in_reply_to_id' : rip,
'in_reply_to_id' : rid,
} }
axios.post(`https://${domain}/api/v1/statuses`, payload, { headers: headers }) axios.post(`https://${domain}/api/v1/statuses`, payload, { headers: headers })
@ -99,7 +74,6 @@ function sendResponse(rid, user, username) {
} }
// follow users who subscribe
function followAction(id, action) { function followAction(id, action) {
let url = `https://${domain}/api/v1/accounts/${id}/${action}` let url = `https://${domain}/api/v1/accounts/${id}/${action}`
@ -116,91 +90,67 @@ function followAction(id, action) {
} }
function filterMentions(text, mentions) {
// filter toot text to remove mentions before checking for the trigger word
// this means if your trigger word is in a user name, you don't get a tsunami of clippy advice
let rawArray = text.replace(/(<([^>]+)>)/gi, "").split(' ')
return rawArray.map( stub => mentions.some( name => `@${name}` === stub) ? "" : stub).toString()
}
// *********************** // ***********************
// STREAMING USER TIMELINE // STREAMING USER TIMELINE
// This is where the action is! // This is where the action is!
// *********************** // ***********************
const ws = new WebSocket(`wss://${domain}/api/v1/streaming?access_token=${access_token}&stream=user`)
function terminate(socket) { // make sure bot is set up correctly each time it starts
console.error(`Terminating connection...`) initiateSettings()
socket.terminate()
console.log(`Terminated`)
}
function listen() { // errors
ws.on('error', err => {
console.error(`WebSocket error: ${err.message}`)
})
console.log(`Listening...`) // check updates and notifications in the stream
const ws = new WebSocket(`wss://${domain}/api/v1/streaming?access_token=${access_token}&stream=user`) ws.on('message', msg => {
let packet = JSON.parse(msg)
let data = JSON.parse(packet.payload)
// make sure bot is set up correctly each time it starts // notifications
initiateSettings(ws) if (packet.event == 'notification') {
// errors // always follow back
ws.on('error', err => { if (data.type == 'follow') {
console.error(`WebSocket error: ${err.message}`) followAction(data.account.id, 'follow')
resetConnection(ws) }
})
// check updates and notifications in the stream if (data.type == 'mention') {
ws.on('message', msg => {
let packet = JSON.parse(msg)
let data = JSON.parse(packet.payload)
// notifications let post = data.status.content
if (packet.event == 'notification') {
// always follow back // check start requests
if (data.type == 'follow') { if (post.match(/\bSTART\b/)) {
followAction(data.account.id, 'follow') followAction(data.account.id, 'follow')
} }
if (data.type == 'mention') { // check stop requests
if (post.match(/\STOP\b/)) {
let post = data.status.content followAction(data.account.id, 'unfollow')
// 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) // updates (posts)
if (packet.event == 'update') { if (packet.event == 'update') {
let rid = data.id let rip = data.id
let user = data.account.acct let user = data.account.username
let username = data.account.username
// get just the account names (@name@domain.tld) // exclude own toots to avoid an infinite loop
let mentions = data.mentions.map( mention => mention.acct) if (data.account.username !== clippy) {
// exclude own toots and @mentions to avoid an infinite loops if ( data.content.includes(topic) ) {
if (username !== clippy && !mentions.includes(clippy)) { sendResponse(rip, user)
// get rid of mentions in case topic is within a username }
let text = filterMentions(data.content, mentions) else if (data.spoiler_text.includes(topic)) {
if ( text.toLowerCase().includes(topic) ) { sendResponse(rip, user)
sendResponse(rid, user, username) }
} else if (data.tags.includes(topic)) {
else if (data.spoiler_text.toLowerCase().includes(topic)) { sendResponse(rip, user)
sendResponse(rid, user, username)
}
else if (data.tags.map(tag => tag.name.toLowerCase()).includes(topic)) {
sendResponse(rid, user, username)
}
} }
} }
}) }
} })
// let's go
listen()

22
package-lock.json generated
View file

@ -1,26 +1,26 @@
{ {
"name": "mastodon-clippy", "name": "mastodon-clippy",
"version": "1.1.1", "version": "1.0.1",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"axios": { "axios": {
"version": "0.21.4", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
"requires": { "requires": {
"follow-redirects": "^1.14.0" "follow-redirects": "^1.10.0"
} }
}, },
"follow-redirects": { "follow-redirects": {
"version": "1.15.6", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg=="
}, },
"ws": { "ws": {
"version": "7.5.10", "version": "7.4.5",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz",
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==" "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g=="
} }
} }
} }

View file

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