summary refs log tree commit diff
path: root/index.js
blob: 28a5a073570f0d10a1181340ee3c17b21e130ea4 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
var fetch = require('node-fetch');
var pg = require('pg');

var DB_USER = process.env.DB_USER || 'ambassador';
var DB_NAME = process.env.DB_NAME || 'mastodon_production';
var DB_PASSWORD = process.env.DB_PASSWORD || '';
var DB_HOST = process.env.DB_HOST || '/var/run/postgresql';
var AMBASSADOR_TOKEN = process.env.AMBASSADOR_TOKEN;
var INSTANCE_HOST = process.env.INSTANCE_HOST;
var BOOSTS_PER_CYCLE = process.env.BOOSTS_PER_CYCLE || 2;
var THRESHOLD_INTERVAL_DAYS = process.env.THRESHOLD_INTERVAL_DAYS || 30;
var BOOST_MAX_DAYS = process.env.BOOST_MAX_DAYS || 5;
var THRESHOLD_CHECK_INTERVAL = process.env.THRESHOLD_CHECK_INTERVAL || 15; // cycles
var CYCLE_INTERVAL = process.env.CYCLE_INTERVAL || 15; // minutes
var BOOST_MIN_HOURS = process.env.BOOST_MIN_HOURS || 12;

var config = {
  user: process.env.DB_USER || 'ambassador',
  database: process.env.DB_NAME || 'mastodon_production',
  password: process.env.DB_PASSWORD || '',
  host: process.env.DB_HOST || '/var/run/postgresql',
  port: 5432, //env var: PGPORT
  max: 2, // max number of clients in the pool
  idleTimeoutMillis: 30000 // how long a client is allowed to remain idle before being closed
};

// Define our threshold (average faves over the past x days)
var thresh_query = `SELECT ceil(avg(favourites_count)) AS threshold
  FROM public_toots
  WHERE
    favourites_count > 1
    AND updated_at > NOW() - INTERVAL '` + THRESHOLD_INTERVAL_DAYS + ` days'`

// Find all toots we haven't boosted yet, but ought to
var query = `SELECT id, updated_at
  FROM public_toots
  WHERE
    favourites_count >= $1
    AND NOT EXISTS (
      SELECT 1
      FROM public_toots AS pt2
      WHERE
        pt2.reblog_of_id = public_toots.id
        AND pt2.account_id = $2
    )
    AND NOT EXISTS (
      SELECT 1
      FROM blocks_ambassador
      WHERE
        public_toots.account_id = blocks_ambassador.account_id
    )
    AND updated_at > NOW() - INTERVAL '` + BOOST_MAX_DAYS + ` days'
    AND updated_at < NOW() - INTERVAL '` + BOOST_MIN_HOURS + ` hours'
  ORDER BY RANDOM()
  LIMIT $3`

// adding this to the WHERE clause would let it skip cases where we're
// blocked by the original poster, but we don't have read privs to blocks
// and I'm not sure I want to change that.  -rt
//
//    AND NOT EXISTS (
//      SELECT 1
//      FROM blocks AS bl1
//      WHERE
//        public_toots.account_id = bl1.account_id
//        AND bl1.target_account_id = 13104
//    )

console.dir('STARTING AMBASSADOR');
console.log('\tDB_USER:', DB_USER);
console.log('\tDB_NAME:', DB_NAME);
console.log('\tDB_PASSWORD:', DB_PASSWORD.split('').map(function() { return "*" }).join(''));
console.log('\tDB_HOST:', DB_HOST);
console.log('\tAMBASSADOR_TOKEN:', AMBASSADOR_TOKEN);
console.log('\tINSTANCE_HOST:', INSTANCE_HOST);
console.log('\tBOOSTS_PER_CYCLE:', BOOSTS_PER_CYCLE);
console.log('\tTHRESHOLD_INTERVAL_DAYS:', THRESHOLD_INTERVAL_DAYS);
console.log('\tBOOST_MAX_DAYS:', BOOST_MAX_DAYS);
console.log('\tTHRESHOLD_CHECK_INTERVAL:', THRESHOLD_CHECK_INTERVAL);
console.log('\tCYCLE_INTERVAL:', CYCLE_INTERVAL);

var g_threshold_downcount = 0;
var g_threshold = 0;

function getThreshold(client, f) {
  if (g_threshold_downcount <= 0 || g_threshold <= 0) {
    console.log('Threshold is stale, recalculating...');
    client.query(thresh_query, [], function (err, result) {
      if (err) {
        throw "error running threshold query: " + err;
      }

      g_threshold = result.rows[0].threshold;
      g_threshold_downcount = THRESHOLD_CHECK_INTERVAL;
      return f(g_threshold);
    });
  } else {
    g_threshold_downcount--;
    console.log('Cycles until next threshold update: ' + g_threshold_downcount);
    return f(g_threshold);
  }
}

function cycle() {
  console.log('Cycle beginning');
  var client = new pg.Client(config);

  client.connect(function (err) {
    if (err) {
      console.error('error connecting to client');
      return console.dir(err);
    }

    whoami(function (account_id) {
      getThreshold(client, function (threshold) {
        console.log('Current threshold: ' + threshold);
        if (threshold < 1) {
          throw "threshold too low: " + threshold;
        }

        client.query(query, [threshold, account_id, BOOSTS_PER_CYCLE], function (err, result) {
          if (err) {
            throw "error running toot query: " + err;
          }

          client.end(function (err) {
            if (err) {
              throw "error disconnecting from client: " + err;
            }
          });

          boost(result.rows);
          console.log('Cycle complete');
        });
      });
    })
  });
}

function whoami(f) {
  fetch(INSTANCE_HOST + '/api/v1/accounts/verify_credentials', {
    headers: {
      'Authorization': 'Bearer ' + AMBASSADOR_TOKEN
    }
  })
  .then(res => res.json())
  .then(result => {
    if (result.id === undefined) {
      console.error('verify_credentials result is undefined');
      process.exit(1);
    }
    console.log('Authenticated as ' + result.id + ' (' + result.display_name + ')');
    return f(result.id);
  })
  .catch(err => {
    console.error(err);
    process.exit(1);
  });
}

function boost(rows) {
  rows.forEach(function(row) {
    console.log('boosting status #' + row.id);
    fetch(INSTANCE_HOST + '/api/v1/statuses/' + row.id + '/reblog', {
      headers: {
        'Authorization': 'Bearer ' + AMBASSADOR_TOKEN
      },
      body: '',
      method: 'POST'
    })
    .then(res => res.json())
    .then(result => {
      if (result.message === 'Validation failed: Reblog of status already exists') {
        console.log('Warning: tried to boost #' + row.id + ' but it had already been boosted by this account.');
      } else if(result.message === 'This action is not allowed') {
        console.log('Warning: tried to boost #' + row.id + ' but the action was not allowed.');
      }
    }).catch(err => {
      console.error(err);
      process.exit(1);
    });
  }
}

cycle();
setInterval(cycle, 1000 * 60 * CYCLE_INTERVAL);