-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.js
154 lines (135 loc) · 4.84 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
import fetch from 'node-fetch'
import {FormData, Blob} from "formdata-node"
import {fileFromPath} from "formdata-node/file-from-path"
import path from 'path'
import _ from 'lodash'
import generateBasicAuth from 'basic-authorization-header'
import { XMLParser } from 'fast-xml-parser'
const TOKEN_NEEDS_TO_BE_OBTAINED = null
export default class RedditImageUploader {
/**
* Upload images to Reddit directly
* @param {{clientID: string, clientSecret: string, username: string, password: string, userAgent: string}|{token: string, userAgent: string}} credentials Credentials for Reddit API
*/
constructor(credentials) {
const configurations = [
['clientID', 'clientSecret', 'username', 'password', 'userAgent'],
['token', 'userAgent']
]
const credentialsProperties = Object.keys(credentials)
switch (configurations.findIndex(configProperties => _.xor(configProperties, credentialsProperties).length === 0)) {
case 0:
// password grant
this.token = TOKEN_NEEDS_TO_BE_OBTAINED
break
case 1:
// use token directly
this.token = credentials.token
break
default:
throw `You must use exact configuration with no extra parameters, provide one of the following configurations for constructor: ${configurations.map(configProperties => configProperties.join(', ')).join('; ')}`
}
this.credentials = credentials
}
async uploadMedia(pathToFile) {
if (this.token === TOKEN_NEEDS_TO_BE_OBTAINED) {
this.token = await loginWithPassword(this.credentials)
}
return await uploadMediaFile(pathToFile, this.token, this.credentials.userAgent)
}
}
// TODO: move everything to class body when private methods implemented in Node
async function loginWithPassword(credentials) {
const body = new FormData()
body.append('grant_type', 'password')
body.append('username', credentials.username)
body.append('password', credentials.password)
const responseRaw = await fetch('https://www.reddit.com/api/v1/access_token', {
method: 'POST',
body,
headers: {
Authorization: generateBasicAuth(credentials.clientID, credentials.clientSecret),
'User-Agent': credentials.userAgent
}
})
const response = await responseRaw.json()
try {
const accessToken = response.access_token
return accessToken
} catch(e) {
console.error('Reddit response:', response)
throw e
}
}
async function uploadMediaFile(mediafile, token, userAgent) {
let file, mimetype, filename
if (typeof mediafile === 'string') {
file = await fileFromPath(mediafile)
filename = path.basename(mediafile)
mimetype = guessMimeType(filename)
//} else if (file instanceof Buffer) {
//mimetype = use mmmagic module?
//filename = 'placeholder. what? extension based on guessed mimetype?
} else {
throw 'You must use string as path to the file to upload it to Reddit.'
}
const { uploadURL, fields, listenWSUrl } = await obtainUploadURL(filename, mimetype, token, userAgent)
const imageURL = await uploadToAWS(uploadURL, fields, file, filename)
return { imageURL, webSocketURL: listenWSUrl }
}
function guessMimeType(filename) {
const extension = path.extname(filename)
const mimeTypes = {
'.png': 'image/png',
'.mov': 'video/quicktime',
'.mp4': 'video/mp4',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
}
return mimeTypes[extension] ?? mimeTypes.jpeg
}
async function obtainUploadURL(filename, mimetype, token, userAgent) {
const bodyForm = new FormData()
bodyForm.append('filepath', filename)
bodyForm.append('mimetype', mimetype)
const uploadURLResponseRaw = await fetch('https://oauth.reddit.com/api/media/asset.json', {
method: 'POST',
body: bodyForm,
headers: {
Authorization: `Bearer ${token}`,
'User-Agent': userAgent
}
})
const uploadURLResponse = await uploadURLResponseRaw.json()
try {
const uploadURL = `https:${uploadURLResponse.args.action}`
const fields = uploadURLResponse.args.fields
const listenWSUrl = uploadURLResponse.asset.websocket_url
return { uploadURL, fields, listenWSUrl }
} catch(e) {
console.error('Reddit API response:', uploadURLResponse)
throw e
}
}
async function uploadToAWS(uploadURL, fields, buffer, filename) {
const bodyForm = new FormData()
fields.forEach(field => bodyForm.append(...Object.values(field)))
bodyForm.append('file', buffer, filename)
const responseRaw = await fetch(uploadURL, {
method: 'POST',
body: bodyForm
})
const response = await responseRaw.text()
try {
const parser = new XMLParser()
const xml = parser.parse(response)
const encodedURL = xml.PostResponse.Location
if (!encodedURL) throw 'No URL returned'
const imageURL = decodeURIComponent(encodedURL)
return imageURL
} catch(e) {
console.error('CDN Response:', response)
throw e
}
}