-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
235 lines (216 loc) · 7.25 KB
/
index.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
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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
// load .env
import dotenv from 'dotenv'
if (process.env.NODE_ENV !== 'production') {
dotenv.config()
}
import http from 'http'
import { URL, URLSearchParams } from 'url'
import express from 'express'
import bodyParser from 'body-parser'
import { ETwitterStreamEvent, TweetStream, TwitterApi, ETwitterApiError } from 'twitter-api-v2'
import utils from './utils.js'
let streamClient, tweetClient, clientUserName
if (process.env.TWITTER_ACCESS_TOKEN) {
tweetClient = new TwitterApi({
appKey: process.env.TWITTER_CONSUMER_KEY,
appSecret: process.env.TWITTER_CONSUMER_SECRET,
accessToken: process.env.TWITTER_ACCESS_TOKEN,
accessSecret: process.env.TWITTER_ACCESS_SECRET,
})
const verifiedUser = await tweetClient.currentUser()
clientUserName = verifiedUser.screen_name
console.log('🌳 connected to twitter user', clientUserName)
console.log('🕊 tweetClient started, waiting for streamClient…')
} else {
console.log('🚑 missing auth for tweetClient, use /sign-in')
}
const app = express()
app.use(bodyParser.json({ limit: '650kb' }))
const server = http.createServer(app)
app.listen(process.env.PORT)
console.log('server is listening to http')
// http
app.get('/', (request, response) => {
console.log('🐢 /')
if (streamClient) {
response.json({
status: 200,
message: '🔮 kinopio-twitter-replies is streaming',
repo: 'https://github.com/kinopio-club/kinopio-twitter-replies'
})
} else {
response.status(503).json({
status: 503,
message: '🚑 kinopio-twitter-replies is waiting to stream…',
repo: 'https://github.com/kinopio-club/kinopio-twitter-replies'
})
}
})
// AUTH STEP 1: sign in to allow tweeting
// You should only need to do this once per environment
// http://localhost:8060/sign-in
// https://twitter-replies.kinopio.club/sign-in
app.get('/sign-in', async (request, response) => {
console.log('🐢 /sign-in')
tweetClient = new TwitterApi({
appKey: process.env.TWITTER_CONSUMER_KEY,
appSecret: process.env.TWITTER_CONSUMER_SECRET
})
const authLink = await tweetClient.generateAuthLink()
response.json({
message: 'store tokens, the auth url will return a PIN. Then pass them to /sign-in-complete',
auth_url: authLink.url,
oauth_token: authLink.oauth_token,
oauth_token_secret: authLink.oauth_token_secret,
})
})
// AUTH STEP 2: complete sign in
// http://localhost:8060/sign-in-complete?pin=123&oauth_token=ABC&oauth_token_secret=XYZ
// https://twitter-replies.kinopio.club/sign-in-complete?pin=123&oauth_token=ABC&oauth_token_secret=XYZ
app.get('/sign-in-complete', async (request, response) => {
console.log('🐢 /sign-in-complete')
const { pin, oauth_token, oauth_token_secret } = request.query
console.log('🌿', pin, oauth_token, oauth_token_secret)
const tweetClient = new TwitterApi({
appKey: process.env.TWITTER_CONSUMER_KEY,
appSecret: process.env.TWITTER_CONSUMER_SECRET,
accessToken: oauth_token,
accessSecret: oauth_token_secret,
})
const { client: loggedClient, accessToken, accessSecret } = await tweetClient.login(pin)
// AUTH STEP 3: copy keys from loggedClient to env
console.log('💖',loggedClient)
response.json({
message: 'update env with TWITTER_ACCESS_TOKEN and TWITTER_ACCESS_SECRET from 💖 log',
oauth_token,
oauth_token_secret,
loggedClient
})
})
// init stream rules
const clearRules = async () => {
let rules = await streamClient.v2.streamRules()
if (rules.data) {
const rulesIds = rules.data.map(rule => rule.id)
await streamClient.v2.updateStreamRules({
delete: {
ids: rulesIds,
},
})
}
rules = await streamClient.v2.streamRules()
console.log('🌚 rules cleared')
}
const addRules = async () => {
const rules = await streamClient.v2.updateStreamRules({
add: [
{ value: '@kinopioclub -from:kinopioclub -is:retweet', tag: 'mentioned' },
{ value: 'kinopio.club -from:kinopioclub -is:retweet', tag: 'space shared' },
]
})
console.log('🌝 rules added', rules)
}
// create tweet space
const tweetById = async (id) => {
const tweet = await tweetClient.v2.singleTweet(id, {
expansions: ['author_id'],
'tweet.fields': ['text'],
'user.fields': ['username']
})
return tweet
}
const createSpace = async (data, kinopioUser) => {
const tweet = data.data
data.conversationTweet = await tweetById(tweet.conversation_id)
console.log('🌸', data.conversationTweet)
utils.createTweetsSpace(data, kinopioUser)
}
// respond to streaming tweets
const replyAndCreateSpace = async (data) => {
const tweet = data.data
const twitterUsername = data.includes.users[0].username
const kinopioUser = await utils.kinopioUser(twitterUsername)
console.log('💁♀️', data, twitterUsername)
let message
if (kinopioUser) {
message = utils.replyMessageSuccess(twitterUsername)
console.log('🍋',message)
createSpace(data, kinopioUser)
} else {
message = utils.replyMessageError(twitterUsername)
}
const options = {
in_reply_to_status_id: tweet.id,
}
if (process.env.NODE_ENV === 'production') {
const reply = await tweetClient.v1.tweet(message, options)
console.log('💌 replied', reply, options, utils.tweetUrl({ tweetId: reply.id_str, username: clientUserName }))
} else {
console.log('✉️ preflight reply', message, options)
}
}
const handleTweet = async (data) => {
const tweet = data.data
const rule = data.matching_rules[0].tag
const isSaveRequest = rule === 'mentioned' && tweet.text.includes('save')
if (isSaveRequest) {
replyAndCreateSpace(data)
// if else ..
// TODO support DMs to save
} else {
// all other mentions, and tweets with spaces
const username = data.includes.users[0].username
// TODO post to discord, pending noise becoming an issue
console.log('💐 TODO post to discord', username, tweet, utils.tweetUrl({ tweetId: tweet.id, username }))
}
}
const listen = async () => {
streamClient = new TwitterApi(process.env.TWITTER_API_BEARER_TOKEN)
console.log('🔮 server is listening to stream')
await clearRules()
await addRules()
try {
// https://github.com/PLhery/node-twitter-api-v2/blob/master/doc/streaming.md
const stream = await streamClient.v2.searchStream({
expansions: ['author_id'],
'user.fields': ['username'],
'tweet.fields': ['conversation_id']
})
stream.on(
ETwitterStreamEvent.Data,
eventData => {
handleTweet(eventData)
},
)
stream.on(
ETwitterStreamEvent.ConnectionClosed,
() => {
console.log('🚒 Connection has been closed')
stream.close()
listen()
},
)
stream.on(
ETwitterStreamEvent.ConnectionError,
error => console.log('🚒 Connection error', error),
)
stream.on(
// Emitted when Twitter sends a signal to keep connection active
ETwitterStreamEvent.DataKeepAlive,
() => console.log('💕 Twitter sent a keep-alive signal'),
)
stream.autoReconnect = true
} catch (error) {
console.error('🚒', error)
}
}
// init listen to stream
if (process.env.NODE_ENV === 'production') {
console.log('waiting to listen to stream…')
setTimeout(() => {
console.log('starting listen to stream')
listen()
}, 5 * 60 * 1000) // wait 5 minute to start streaming
} else {
listen()
}