Skip to content

Commit

Permalink
Added tweet templating
Browse files Browse the repository at this point in the history
  • Loading branch information
RagePeanut committed May 1, 2018
1 parent 089d13b commit 4634a86
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 71 deletions.
29 changes: 25 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Tweem (0.1.3)
# Tweem (0.1.4)
Tweem is a bot that automatically tweets all the resteems and/or recent posts of specified accounts.

## Deploy
Expand Down Expand Up @@ -31,8 +31,6 @@ Here are all the configuration possibilities:
* **request_nodes:** list of RPC nodes to be used by the app to get posts informations (those need to be full nodes)
* **settings:**
* ***allowed_links:*** replace default 'steemit.com' links by the link from the posts' apps (default: true)
* ***include_tags:*** include tags in tweets (default: true)
* ***include_title:*** include title in tweets (default: true)
* ***mentions:***
* ***escape_starting_mention:*** escape a mention by adding a '.' in front of it if it is the first word of a tweet (default: true)
* ***remove_mentions:*** remove mentions completely (default: false)
Expand All @@ -48,16 +46,39 @@ Here are all the configuration possibilities:
* ***tweet_resteems:*** tweet resteems from the specified accounts (default: true)
* **steem_accounts:** list of Steem accounts to watch (default: ['ragepeanut'])
* **stream_nodes:** list of RPC nodes to be used by the app to stream operations (those can be low memory nodes)
* **template:** template for the tweet (explained in 'Create your own template')
* **tweet_retry_timeout:** time in milliseconds to wait for before retrying to tweet if it failed (default: 10000)

## Create your own template
**Tweems** aims to be the most configurable sharing bot on the **Steem** blockchain, that's why you can change how your tweets will look by changing their template. Let's take a look at the default template to understand what's happening.
```
{{title::2}} %%by @{{author}}::1%% {{tags::3}}
```
Every part of the tweet (except the 'by' part) is defined by `{{DATA::IMPORTANCE}}`, `DATA` is the data indicator, it can be replaced by 'title' or 'tags'. You can't have the same data indicator twice in a template. `IMPORTANCE` is a number that specifies how important this part of the tweet is. If a message is too long to be twitted (more than 280 characters), the part of the tweet that has the lowest `IMPORTANCE` number will get changed to fit the maximum message length. The `%%by @{{author}}::1%%` may be the hardest part of this template to understand, it corresponds to the 'by' part of the tweet and will only be in the tweet when you resteem a post. It is defined by `%%TEXT{{author}}TEXT::IMPORTANCE%%`. `TEXT` is any piece of text you would like to see in the tweet, `{{author}}` is the author username (right now, 'author' is the only value you can put between these curly brackets) and `IMPORTANCE` has already been explained before. You can only put on 'by' part in your template. The link is not defined in the template, that's because it must always be at the end of the tweet for **Twitter** to handle it well. Here are a few examples along with their expected results to help you understand how the templating works.
* `{{title::2}} %%by @{{author}}::1%% {{tags::3}}`<br>
**Post:** `TITLE TAGS`<br>
**Resteem:** `TITLE by @AUTHOR TAGS`

* `%%I just resteemed ::1%%{{title::2}} {{tags::3}}`<br>
**Post:** `TITLE TAGS`<br>
**Resteem:** `I just resteemed TITLE TAGS`

* `{{title::2}} %%by {{author}} was amazing!::1%%`<br>
**Post:** `TITLE`<br>
**Resteem:** `TITLE by AUTHOR was amazing!`

* `Check this post out! {{tags::1}}`<br>
**Post:** `Check this post out! TAGS`<br>
**Resteem:** `Check this post out! TAGS`

## Special thanks to
**Steemit** for [steem.js](https://github.com/steemit/steem-js)<br>
**Tolga Tezel** for [Twit](https://github.com/ttezel/twit)

## Social networks
**Steemit:** https://steemit.com/@ragepeanut <br>
**Busy:** https://busy.org/@ragepeanut <br>
**Twitter:** https://twitter.com/RagePeanut_ <br>
**Twitter:** [https://twitter.com/RagePeanut_](https://twitter.com/RagePeanut_) <br>
**Steam:** http://steamcommunity.com/id/ragepeanut/

### Follow me on [Steemit](https://steemit.com/@ragepeanut) or [Busy](https://busy.org/@ragepeanut) to be informed on my new releases and projects.
163 changes: 100 additions & 63 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,75 +131,112 @@ function treatOperation(author, permlink, type) {
} catch(err) {
return reject(err);
}
let message = '';
// Title
if(settings.include_title && result.title.length > 0) {
// Mentions
// If set to true, completely removes the mentions in the title (e.g. 'Hello @ragepeanut !' --> 'Hello !')
if(settings.mentions.remove_mentions) result.title = result.title.replace(/( )?@[a-zA-Z0-9._-]+( )?/g, (match, firstSpace, secondSpace) => { return firstSpace || secondSpace});
// If set to true, removes the @ character from mentions (e.g. 'Bye @ragepeanut !' --> 'Bye ragepeanut !')
else if(settings.mentions.remove_mentions_at_char) result.title = result.title.replace(/@([a-zA-Z0-9._-]+)/g, '$1');
// If set to true, escapes a mention if it is the first word of the title (e.g. '@ragepeanut isn\'t a real peanut :O' --> '.@ragepeanut isn\'t an real peanut :O')
else if(settings.mentions.escape_starting_mention && result.title[0] === '@') result.title = '.' + result.title;
message += result.title + ' ';
}
// Tags
if(settings.include_tags) {
let tags = metadata.tags || [result.category];
// If set to true, removes any duplicate tag from the tags array
if(settings.tags.check_for_duplicate) {
const tmpTags = [];
tags.forEach(tag => {
if(!tmpTags.includes(tag)) tmpTags.push(tag);
});
tags = tmpTags;

const regex = /{{([^{]+)::(\d+)}}|%%(.+)::(\d+)%%/g;
const message = {
by: {
content: '',
importance: 0
},
tags: {
arr: [],
content: '',
importance: 0
},
title: {
content: '',
importance: 0
}
// If set to an integer, takes only the X first tags
if(settings.tags.limit) tags = tags.slice(0, settings.tags.limit);
// Unshifting to put a # character in front of the first tag
tags.unshift('');
message += tags.reduce((accumulator, tag) => tag.length > 0 ? accumulator + '#' + tag + ' ' : accumulator);
// Checking if the message is going to be longer than the maximum length
if(message.length + LINK_LENGTH > MAX_TWEET_LENGTH) {
tags.shift();
let neededLength = message.length + LINK_LENGTH - MAX_TWEET_LENGTH;
// If set to true, removes tags by order of importance (last tag removed first)
if(settings.tags.remove_tags_by_order) {
for(let i = tags.length - 1; i >= 0 && neededLength > 0; i--) {
message = message.replace('#' + tags[i] + ' ', '');
neededLength -= tags[i].length + 2;
}
// If set to true, removes tags by the opposite order of importance (first tag removed first)
} else if(settings.tags.remove_tags_by_order_opposite) {
for(let i = 0; i < tags.length && neededLength > 0; i++) {
message = message.replace('#' + tags[i] + ' ', '');
neededLength -= tags[i].length + 2;
}
let messageLength = LINK_LENGTH + settings.template.replace(regex, '').length;
let match;
console.log(message.tags.content);
while(match = regex.exec(settings.template)) {
switch(match[1] || match[3]) {
case 'tags':
message.tags.importance = parseInt(match[2]);
let tags = metadata.tags || [match.category];
// If set to true, removes any duplicate tag from the tags array
if(settings.tags.check_for_duplicate) {
const tmpTags = [];
tags.forEach(tag => {
if(!tmpTags.includes(tag)) tmpTags.push(tag);
});
tags = tmpTags;
}
// If set to true, removes tags by length (smallest removed first)
} else if(settings.tags.remove_tags_by_length) {
tags = tags.sort((a, b) => a.length - b.length);
for(let i = 0; i < tags.length && neededLength > 0; i++) {
message = message.replace('#' + tags[i] + ' ', '');
neededLength -= tags[i].length + 2;
// If set to an integer, takes only the X first tags
if(settings.tags.limit) tags = tags.slice(0, settings.tags.limit);
message.tags.arr = tags;
message.tags.content = tags.map(tag => '#' + tag).join(' ');
messageLength += message.tags.content.length;
break;
case 'title':
message.title.importance = parseInt(match[2]);
message.title.content = result.title;
// Mentions
// If set to true, completely removes the mentions in the title (e.g. 'Hello @ragepeanut !' --> 'Hello !')
if(settings.mentions.remove_mentions) message.title.content = message.title.content.replace(/( )?@[a-zA-Z0-9._-]+( )?/g, (match, firstSpace, secondSpace) => firstSpace || secondSpace);
// If set to true, removes the @ character from mentions (e.g. 'Bye @ragepeanut !' --> 'Bye ragepeanut !')
else if(settings.mentions.remove_mentions_at_char) message.title.content = message.title.content.replace(/@([a-zA-Z0-9._-]+)/g, '$1');
// If set to true, escapes a mention if it is the first word of the title (e.g. '@ragepeanut isn\'t a real peanut :O' --> '.@ragepeanut isn\'t an real peanut :O')
else if(settings.mentions.escape_starting_mention && message.title.content[0] === '@') message.title.content = '.' + message.title.content;
messageLength += message.title.content.length;
break;
default:
message.by.importance = parseInt(match[4]);
if(type === 'reblog') {
message.by.content = match[3].replace(/{{([^{]+)}}/g, (match, variable) => {
try {
return eval(variable);
} catch(err) {
console.err('Error: the variable \'' + variable + '\' doesn\'t exist. Treating it as a string.');
return '{{' + variable + '}}';
}
});
messageLength += message.by.content.length;
}
// If set to true, removes tags by length (longest removed first)
} else if(settings.tags.remove_tags_by_length_opposite) {
tags = tags.sort((a, b) => b.length - a.length);
for(let i = 0; i < tags.length && neededLength > 0; i++) {
message = message.replace('#' + tags[i] + ' ', '');
neededLength -= tags[i].length + 2;
}
};
const leastToMostImportant = Object.keys(message).sort((a, b) => message[b].importance < message[a].importance);
while(messageLength > MAX_TWEET_LENGTH && leastToMostImportant.length > 0) {
const part = leastToMostImportant.shift();
let neededLength = messageLength - MAX_TWEET_LENGTH;
switch(part) {
case 'by':
messageLength -= message.by.content.length;
message.by.content = '';
break;
case 'tags':
let removalOrder = message.tags.arr.slice(0);
// If set to true, removes tags by order of importance (last tag removed first)
if(settings.tags.remove_tags_by_order) removalOrder.reverse();
// If set to true, removes tags by length (smallest removed first)
else if(settings.tags.remove_tags_by_length) removalOrder.sort((a, b) => a.length - b.length);
// If set to true, removes tags by length (longest removed first)
else if(settings.tags.remove_tags_by_length_opposite) removalOrder.sort((a, b) => b.length - a.length);
// If set to true, removes tags by the opposite order of importance (first tag removed first)
// If set to false, don't remove any tag
else if(!settings.tags.remove_tags_by_order_opposite) removalOrder = [];
while(neededLength > 0 && removalOrder.length > 0) {
const toRemove = removalOrder.shift();
message.tags.arr.splice(message.tags.arr.findIndex(tag => tag === toRemove), 1);
messageLength -= message.tags.content.length;
message.tags.content = message.tags.arr.map(tag => '#' + tag).join(' ');
messageLength += message.tags.content.length;
neededLength = messageLength - MAX_TWEET_LENGTH;
}
}
break;
default:
message.title.content = message.title.content.substr(0, message.title.content.length - neededLength - 3) + '...';
messageLength -= neededLength;
break;
}
}
// If the message is too long, trims the title
if(message.length + LINK_LENGTH > MAX_TWEET_LENGTH) {
let neededLength = message.length + LINK_LENGTH - MAX_TWEET_LENGTH;
message = message.replace(result.title, result.title.substr(0, result.title.length - neededLength - 3) + '...');
}
// First parameter (app): checking for all the known ways of specifying an app, if none of them exists the app is set to undefined
message += getWebsite(metadata.community || (metadata.app && (metadata.app.name || metadata.app.split('/')[0])) || undefined, result.author, result.permlink, result.url, metadata.tags, result.body);
tweet(message);
const tweetContent = (settings.template.replace(/%%.+%%/g, message.by.content)
.replace(/{{([^{]+)::\d+}}/g, (match, content) => message[content].content)
+ ' ' + getWebsite(metadata.community || (metadata.app && (metadata.app.name || metadata.app.split('/')[0])) || undefined, result.author, result.permlink, result.url, metadata.tags, result.body))
.replace(/ +/g, ' ');
tweet(tweetContent);
}
});
}).catch(err => {
Expand Down
3 changes: 1 addition & 2 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@
"utopian": true,
"zappl": true
},
"include_tags": true,
"include_title": true,
"mentions": {
"escape_starting_mention": true,
"remove_mentions": false,
Expand All @@ -37,6 +35,7 @@
"remove_tags_by_order": true,
"remove_tags_by_order_opposite": false
},
"template": "{{title::2}} %%by @{{author}}::1%% {{tags::3}}",
"tweet_posts": true,
"tweet_resteems": true
},
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tweem",
"version": "0.1.3",
"version": "0.1.4",
"description": "A bot tweeting all the resteems and/or recent posts of specified accounts.",
"main": "app.js",
"scripts": {
Expand Down

0 comments on commit 4634a86

Please sign in to comment.