-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathclient.js
154 lines (140 loc) · 5.65 KB
/
client.js
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
/* eslint-env browser */
import Chain from 'drand-client/chain.js'
import PollingWatcher from 'drand-client/polling-watcher.js'
import { controllerWithParent } from 'drand-client/abort.js'
import { hexToBuffer, bufferToHex, sha256, retry, pause } from './util.js'
// Maximum retries for getting the latest round before it fails.
const LATEST_ROUND_RETRIES = 5
// Backoff interval in ms between retries - doubled each retry.
const LATEST_ROUND_RETRY_BACKOFF = 50
// Expected delay in ms between randomness being generated and it being
// available on twitter, reduce this for faster latest randomness but more
// requests to twitter.
const LATEST_ROUND_DELAY = 650
// Maximum number of tweets that can be requested from the twitter API.
const MAX_TWEET_COUNT = 200
export default class Client {
constructor (config) {
config = config || {}
if (!config.screenName) throw new Error('screen name required')
if (!config.bearerToken) throw new Error('bearer token required')
if (!config.chainInfo) throw new Error('chain info required')
config.latestRoundDelay = config.latestRoundDelay || LATEST_ROUND_DELAY
config.latestRoundRetries = config.latestRoundRetries || LATEST_ROUND_RETRIES
config.latestRoundRetryBackoff = config.latestRoundRetryBackoff || LATEST_ROUND_RETRY_BACKOFF
this._config = config
this._watcher = new PollingWatcher(this, config.chainInfo)
this._controllers = []
}
async info () {
return this._config.chainInfo
}
async get (round, options) {
options = options || {}
const now = new Date()
const latestRound = this.roundAt(now.getTime())
round = round || latestRound
const controller = controllerWithParent(options.signal)
this._controllers.push(controller)
try {
let rand
if (round === latestRound) {
const latestRoundTime = this._roundTime(latestRound)
const delay = this._config.latestRoundDelay
if (now - latestRoundTime < delay) {
// console.log('in delay period, pausing for ', latestRoundTime + delay - now, 'ms')
await pause(latestRoundTime + delay - now, { signal: options.signal })
}
rand = await retry(() => this._get(round, { ...options, count: 2 }), {
times: this._config.latestRoundRetries,
backoff: this._config.latestRoundRetryBackoff,
signal: options.signal
})
} else {
rand = await this._get(round, options)
}
return rand
} finally {
this._controllers = this._controllers.filter(c => c !== controller)
controller.abort()
}
}
async _get (round, options) {
let maxID
while (true) {
let url = `https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=${this._config.screenName}&count=${options.count || MAX_TWEET_COUNT}&trim_user=true&include_rts=false&exclude_replies=true&tweet_mode=extended`
if (maxID) {
url += '&max_id=' + maxID
}
// console.log('_get', url)
const res = await fetch(url, {
headers: { Authorization: `Bearer ${this._config.bearerToken}` },
signal: options.signal
})
if (!res.ok) {
throw new Error('getting tweets: HTTP status: ' + res.status)
}
const data = await res.json()
const beacons = data
.map(t => {
try {
return JSON.parse(t.full_text)
} catch (err) {
console.warn('parsing tweet', err)
}
})
.filter(Boolean)
const beaconIndex = beacons.findIndex(b => b.round === round)
if (beaconIndex !== -1) {
const beacon = beacons[beaconIndex]
let prevBeacon = beacons.find(b => b.round === round - 1)
// TODO: make more DRY
if (!prevBeacon) {
const url = `https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=${this._config.screenName}&count=2&trim_user=true&include_rts=false&exclude_replies=true&tweet_mode=extended&max_id=${data[beaconIndex].id_str}`
const res = await fetch(url, {
headers: { Authorization: `Bearer ${this._config.bearerToken}` },
signal: options.signal
})
if (!res.ok) {
throw new Error('getting historic previous beacon tweet: HTTP status: ' + res.status)
}
const prevData = await res.json()
if (prevData.length !== 2) {
throw new Error(`beacon not found ${round - 1}`)
}
prevBeacon = JSON.parse(prevData[prevData.length - 1].full_text)
if (prevBeacon.round !== round - 1) {
throw new Error(`expected previous beacon ${round - 1} but got ${prevBeacon.round}`)
}
}
beacon.previous_signature = prevBeacon.signature
const hash = await sha256(hexToBuffer(beacon.signature))
beacon.randomness = bufferToHex(hash)
return beacon
}
// Passed the round we were looking for
if (beacons.length && beacons[beacons.length - 1].round < round) {
throw new Error(`beacon not found ${round}`)
}
// No more results
if (!data.length || maxID === data[data.length - 1].id_str) {
throw new Error(`beacon not available ${round}`)
}
maxID = data[data.length - 1].id_str
}
}
async * watch (options) {
yield * this._watcher.watch(options)
}
roundAt (time) {
return Chain.roundAt(time, this._config.chainInfo.genesis_time * 1000, this._config.chainInfo.period * 1000)
}
_roundTime (round) {
return Chain.roundTime(round, this._config.chainInfo.genesis_time * 1000, this._config.chainInfo.period * 1000)
}
async close () {
this._controllers.forEach(c => c.abort())
this._controllers = []
await this._watcher.close()
}
}