Version 0.1.1 New Features
Next phase of the ethical voting bot release.
Changes
Support for main bot user
Originally, it was a collective vote from many users. Now we are including the bot user itself. This allows the following:
- Bot can now send comments/replies to protagonists, antagonists, and victims.
- Messages can be configured via template
- Bot now supports voting for users posts and comments (from the bot. not collective votes)
- Bot supports delegation
Voting threshold now enforced
- Voting thresholds are now functioning. Users do not vote beyond their specified voting power threshold. Yay!
Logging
- Logging was updated to be more meaningful.
- Redundant logging was removed.
Orchestration
- Orchestration configuration for kubernetes clusters. This will allow us to deploy to the application, provisioning, and any dependencies to a cluster simultaneously. It's da bomb.
Details
diff --git a/Procfile b/Procfile
index 6c983c9..3a76fa8 100644
--- a/Procfile
+++ b/Procfile
@@ -1 +1,2 @@
web: node app/index.js
+worker: node app/bot.js
diff --git a/app/bot.js b/app/bot.js
new file mode 100644
index 0000000..e5dfdc0
--- /dev/null
+++ b/app/bot.js
@@ -0,0 +1,4 @@
+const bot = require('./helpers/bot')
+
+
+bot.run();
\ No newline at end of file
Created a new standalone execution file and heroku configuration for separate execution.
diff --git a/app/controllers/preferences.js b/app/controllers/preferences.js
index cb2564a..99f1fe0 100644
--- a/app/controllers/preferences.js
+++ b/app/controllers/preferences.js
@@ -55,7 +55,6 @@ function handle_prefs_from_database(username, res) {
return models.Preferences.findOne({where: { username: username } })
.then(prefs => {
var preferences = prefs.dataValues
- console.log(preferences)
res.render('pages/index', {
redirect: 'https://we-resist-bot.herokuapp.com/',
username: username,
This change removes redundant logging from the controller that was initially used for determining the preferences model.
diff --git a/app/helpers/bot/task.js b/app/helpers/bot/task.js
index 3fbd502..e332754 100644
--- a/app/helpers/bot/task.js
+++ b/app/helpers/bot/task.js
@@ -2,12 +2,16 @@
const Promise = require('bluebird')
const steem = Promise.promisifyAll(require('steem'))
-const config = require('../../config')
+const sc2 = Promise.promisifyAll(require('sc2-sdk'))
+const { user, wif } = require('../../config')
const moment = require('moment')
const schedule = require('node-schedule')
const Sequelize = require('sequelize')
const models = require('../../../models')
const Op = Sequelize.Op;
+const Handlebars = require('handlebars')
+const fs = Promise.promisifyAll(require('fs'))
+const path = require('path')
Including dependencies required for templating messages used in comments and adding the ability for the bot to function independently.
const UNVOTE_WEIGHT = 0
@@ -15,6 +19,29 @@ module.exports = {
execute
}
+const SECONDS_PER_HOUR = 3600
+const PERCENT_PER_DAY = 20
+const HOURS_PER_DAY = 24
+const MAX_VOTING_POWER = 10000
+const DAYS_TO_100_PERCENT = 100 / PERCENT_PER_DAY
+const SECONDS_FOR_100_PERCENT = DAYS_TO_100_PERCENT * HOURS_PER_DAY * SECONDS_PER_HOUR
+const RECOVERY_RATE = MAX_VOTING_POWER / SECONDS_FOR_100_PERCENT
+const DEFAULT_THRESHOLD = 9500
+
+
+function current_voting_power(vp_last, last_vote) {
+ var seconds_since_vote = moment().add(7, 'hours').diff(moment(last_vote), 'seconds')
+ return (RECOVERY_RATE * seconds_since_vote) + vp_last
+}
+
+function time_needed_to_recover(voting_power, threshold) {
+ return RECOVERY_RATE * (threshold - voting_power)
+}
Adding threshold management functions and constants
+
+function loadTemplate(template) {
+ return fs.readFileAsync(template, 'utf8')
+}
+
The above change loads a template from the filesystem to be used as a message.
/**
@@ -74,7 +114,8 @@ function processVote(vote) {
*/
function list_of_resisters() {
return models.Preferences.findAll( {
- attributes: [ 'username', 'wif', 'upvoteWeight', 'downvoteWeight', 'threshold' ]
+ attributes: [ 'username', 'wif', 'upvoteWeight', 'downvoteWeight', 'threshold' ],
+ logging: (query) => {}
})
}
Removing logging of every single SQL query
@@ -83,31 +124,91 @@ function is_active(resister) {
}
function processDownvote(vote) {
+ console.log("Upvoting ", vote)
return collectiveUpvote(vote.author, vote.permlink)
}
function processUpvote(vote) {
- if (vote.is_for_grumpy()) {
- return collectiveDownvote(vote.author, vote.permlink)
- }
- return false
+ return vote.is_author_grumpy()
+ .then((is_grumpy) => {
+ if (is_grumpy) { // Test for self-vote
+ console.log("Downvoting ", vote)
+ return collectiveDownvote(vote.author, vote.permlink)
+ }
+
+ // Not a self-vote
+ Promise.reject("Not a self vote")
+ })
+ .catch(err => {
+ console.log(err)
+ })
}
function processUnvote(vote) {
- if (!vote.is_grumpy()) {
+ if (!vote.is_voter_grumpy()) {
return false
}
return collectiveUnvote(author, permlink)
}
The above is to add voting refinements based on whether the vote was a self-vote or not. Logging is also refined here.
+function invite(author, permlink) {
+ return reply(author, permlink, "invite");
+}
+
+function reply(author, permlink, type) {
+ var context = {
+ }
+
+ return loadTemplate(path.join(__dirname, '..', 'templates', `${type}.hb`))
+ .then((template) => {
+ var templateSpec = Handlebars.compile(template)
+ return templateSpec(context)
+ })
+ .then((message) => {
+ var new_permlink = 're-' + author
+ + '-' + permlink
+ + '-' + new Date().toISOString().replace(/[^a-zA-Z0-9]+/g, '').toLowerCase();
+ steem.broadcast.commentAsync(
+ wif,
+ author, // Leave parent author empty
+ permlink, // Main tag
+ user, // Author
+ new_permlink, // Permlink
+ new_permlink,
+ message, // Body
+ { tags: [], app: 'we-resist-bot/0.1.0' }
+ ).then((results) => {
+ console.log(results)
+ })
+ })
+}
function downvote(author, permlink, resister) {
- return vote(author, permlink, resister, resister.downvoteWeight * -1)
+ return vote(author, permlink, resister, resister.downvoteWeight * -100)
+ .then((promise) => { return reply(author, permlink, "downvote") });
}
function upvote(author, permlink, resister) {
- return vote(author, permlink, resister, resister.upvoteWeight)
+ var recovery_wait = 0
+ return steem.api.getAccountsAsync([ resister.username ]).then((account) => {
+ var voting_power = current_voting_power(account.voting_power, account.last_vote_time)
+ recovery_wait = time_needed_to_recover(voting_power, resister.threshold) / 60
+ return account
+ })
+ .then((account) => {
+ // Reschedule vote
+ if (recovery_wait > 0) {
+ var later = moment().add(recovery_wait, 'minutes').toDate()
+ console.log("Rescheduling ", recovery_wait, " minutes to recover")
+ schedule.scheduleJob(later, function() {
+ upvote(author, permlink, resister, resister.upvoteWeight * 100)
+ })
+ return account
+ }
+ return vote(author, permlink, resister, resister.upvoteWeight * 100)
+ .then((promise) => { return reply(author, permlink, "upvote") });
+ })
}
function unvote(author, permlink, resister) {
@@ -142,17 +243,46 @@ function collectiveUnvote(author, permlink) {
return list_of_resisters().each((resister) => { return unvote(author, permlink, resister) })
}
The above is an implementation to add replies based on behavior of voting/downvoting. These replies make use of message templating.
+function processComment(comment) {
+ return list_of_resisters()
+ .filter((resister) => comment.author == resister.username)
+ .each((resister) => {
+ var recovery_wait = 0
+ return steem.api.getAccountsAsync([ user ]).then((account) => {
+ var voting_power = current_voting_power(account.voting_power, account.last_vote_time)
+ recovery_wait = time_needed_to_recover(voting_power, DEFAULT_THRESHOLD) / 60
+ return account
+ })
+ .then((account) => {
+ // Reschedule vote
+ if (recovery_wait > 0) {
+ var later = moment().add(recovery_wait, 'minutes').toDate()
+ console.log("Rescheduling ", recovery_wait, " minutes to recover")
+ schedule.scheduleJob(later, function() {
+ processComment(comment)
+ })
+ return account
+ }
+ return vote(comment.author, comment.permlink, { username: user, wif: wif }, 10000)
+ })
+ })
+}
The above change instructs the voting bot to vote on comments/posts and to handle voting power threshold.
diff --git a/orchestration/charts/we-resist-bot/Chart.yaml b/orchestration/charts/we-resist-bot/Chart.yaml
new file mode 100644
index 0000000..f325eeb
--- /dev/null
+++ b/orchestration/charts/we-resist-bot/Chart.yaml
@@ -0,0 +1,4 @@
+apiVersion: v1
+description: A Helm chart for Kubernetes
+name: we-resist-bot
+version: 0.1.0
diff --git a/orchestration/charts/we-resist-bot/templates/NOTES.txt b/orchestration/charts/we-resist-bot/templates/NOTES.txt
new file mode 100644
index 0000000..3e642a3
--- /dev/null
+++ b/orchestration/charts/we-resist-bot/templates/NOTES.txt
@@ -0,0 +1,19 @@
+1. Get the application URL by running these commands:
+{{- if .Values.ingress.enabled }}
+{{- range .Values.ingress.hosts }}
+ http://{{ . }}
+{{- end }}
+{{- else if contains "NodePort" .Values.service.type }}
+ export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "we-resist-bot.fullname" . }})
+ export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
+ echo http://$NODE_IP:$NODE_PORT
+{{- else if contains "LoadBalancer" .Values.service.type }}
+ NOTE: It may take a few minutes for the LoadBalancer IP to be available.
+ You can watch the status of by running 'kubectl get svc -w {{ template "we-resist-bot.fullname" . }}'
+ export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "we-resist-bot.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
+ echo http://$SERVICE_IP:{{ .Values.service.externalPort }}
+{{- else if contains "ClusterIP" .Values.service.type }}
+ export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "we-resist-bot.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
+ echo "Visit http://127.0.0.1:8080 to use your application"
+ kubectl port-forward $POD_NAME 8080:{{ .Values.service.internalPort }}
+{{- end }}
diff --git a/orchestration/charts/we-resist-bot/templates/_helpers.tpl b/orchestration/charts/we-resist-bot/templates/_helpers.tpl
new file mode 100644
index 0000000..ba4b569
--- /dev/null
+++ b/orchestration/charts/we-resist-bot/templates/_helpers.tpl
@@ -0,0 +1,16 @@
+{{/* vim: set filetype=mustache: */}}
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "we-resist-bot.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+*/}}
+{{- define "we-resist-bot.fullname" -}}
+{{- $name := default .Chart.Name .Values.nameOverride -}}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
diff --git a/orchestration/charts/we-resist-bot/templates/deployment.yaml b/orchestration/charts/we-resist-bot/templates/deployment.yaml
new file mode 100644
index 0000000..52c67ea
--- /dev/null
+++ b/orchestration/charts/we-resist-bot/templates/deployment.yaml
@@ -0,0 +1,36 @@
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: {{ template "we-resist-bot.fullname" . }}
+ labels:
+ app: {{ template "we-resist-bot.name" . }}
+ chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
+ release: {{ .Release.Name }}
+ heritage: {{ .Release.Service }}
+spec:
+ replicas: {{ .Values.replicaCount }}
+ template:
+ metadata:
+ labels:
+ app: {{ template "we-resist-bot.name" . }}
+ release: {{ .Release.Name }}
+ spec:
+ containers:
+ - name: {{ .Chart.Name }}
+ image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
+ env:
+ - name: STEEM_NAME
+ value: "{{ .Values.steem.name }}"
+ - name: STEEM_WIF
+ value: "{{ .Values.steem.wif }}"
+ - name: DATABASE_URL
+ value: "{{ .Values.persistence.url }}"
+ ports:
+ - containerPort: {{ .Values.service.internalPort }}
+ resources:
+{{ toYaml .Values.resources | indent 12 }}
+ {{- if .Values.nodeSelector }}
+ nodeSelector:
+{{ toYaml .Values.nodeSelector | indent 8 }}
+ {{- end }}
diff --git a/orchestration/charts/we-resist-bot/templates/ingress.yaml b/orchestration/charts/we-resist-bot/templates/ingress.yaml
new file mode 100644
index 0000000..88960cd
--- /dev/null
+++ b/orchestration/charts/we-resist-bot/templates/ingress.yaml
@@ -0,0 +1,32 @@
+{{- if .Values.ingress.enabled -}}
+{{- $serviceName := include "we-resist-bot.fullname" . -}}
+{{- $servicePort := .Values.service.externalPort -}}
+apiVersion: extensions/v1beta1
+kind: Ingress
+metadata:
+ name: {{ template "we-resist-bot.fullname" . }}
+ labels:
+ app: {{ template "we-resist-bot.name" . }}
+ chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
+ release: {{ .Release.Name }}
+ heritage: {{ .Release.Service }}
+ annotations:
+ {{- range $key, $value := .Values.ingress.annotations }}
+ {{ $key }}: {{ $value | quote }}
+ {{- end }}
+spec:
+ rules:
+ {{- range $host := .Values.ingress.hosts }}
+ - host: {{ $host }}
+ http:
+ paths:
+ - path: /
+ backend:
+ serviceName: {{ $serviceName }}
+ servicePort: {{ $servicePort }}
+ {{- end -}}
+ {{- if .Values.ingress.tls }}
+ tls:
+{{ toYaml .Values.ingress.tls | indent 4 }}
+ {{- end -}}
+{{- end -}}
diff --git a/orchestration/charts/we-resist-bot/templates/service.yaml b/orchestration/charts/we-resist-bot/templates/service.yaml
new file mode 100644
index 0000000..d83e6a1
--- /dev/null
+++ b/orchestration/charts/we-resist-bot/templates/service.yaml
@@ -0,0 +1,19 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ template "we-resist-bot.fullname" . }}
+ labels:
+ app: {{ template "we-resist-bot.name" . }}
+ chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
+ release: {{ .Release.Name }}
+ heritage: {{ .Release.Service }}
+spec:
+ type: {{ .Values.service.type }}
+ ports:
+ - port: {{ .Values.service.externalPort }}
+ targetPort: {{ .Values.service.internalPort }}
+ protocol: TCP
+ name: {{ .Values.service.name }}
+ selector:
+ app: {{ template "we-resist-bot.name" . }}
+ release: {{ .Release.Name }}
diff --git a/orchestration/charts/we-resist-bot/values.yaml b/orchestration/charts/we-resist-bot/values.yaml
new file mode 100644
index 0000000..9411a7d
--- /dev/null
+++ b/orchestration/charts/we-resist-bot/values.yaml
@@ -0,0 +1,45 @@
+# Default values for we-resist-bot.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+replicaCount: 1
+image:
+ repository: r351574nc3/we-resist-bot
+ tag: latest
+ pullPolicy: Always
+
+service:
+ type: ClusterIP
+ internalPort: 5000
+ externalPort: 5000
+
+steem:
+ name: please fill this in
+ wif: please fill this in
+
+persistence:
+ url: please fill this in
+
+ingress:
+ enabled: false
+ # Used to create an Ingress record.
+ hosts:
+ - chart-example.local
+ annotations:
+ # kubernetes.io/ingress.class: nginx
+ # kubernetes.io/tls-acme: "true"
+ tls:
+ # Secrets must be manually created in the namespace.
+ # - secretName: chart-example-tls
+ # hosts:
+ # - chart-example.local
+resources: {}
+ # We usually recommend not to specify default resources and to leave this as a conscious
+ # choice for the user. This also increases chances charts run on environments with little
+ # resources, such as Minikube. If you do want to specify resources, uncomment the following
+ # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
+ # limits:
+ # cpu: 100m
+ # memory: 128Mi
+ # requests:
+ # cpu: 100m
+ # memory: 128Mi
Kubernetes orchestration configuration
Posted on Utopian.io - Rewarding Open Source Contributors
Moar like this!
Ezekiel 25:17 comes to mind
Roadmap
That's what's coming up
@r351574nc3, I like your contribution to open source project, so I upvote to support you.
Thank you so much! It's appreciated.
Thank you for the contribution. It has been approved.
You can contact us on Discord.
[utopian-moderator]
Hey @r351574nc3 I am @utopian-io. I have just upvoted you!
Achievements
Community-Driven Witness!
I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!
Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x
Thanks