mirror of
https://github.com/byulmaru/quesdon
synced 2024-11-27 14:28:04 +09:00
wip
This commit is contained in:
parent
77ffc2770b
commit
2518105e2e
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
dist/
|
||||
node_modules/
|
||||
node_modules/
|
||||
.env
|
3
package-lock.json
generated
3
package-lock.json
generated
@ -4972,6 +4972,9 @@
|
||||
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
|
||||
"dev": true
|
||||
},
|
||||
"oauth-1.0a": {
|
||||
"version": "github:rinsuki/oauth-1.0a#b6a8b9cfd4f9621c03a57379f4f04ce79e2f4248"
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
|
@ -63,6 +63,7 @@
|
||||
"mongoose": "^4.13.6",
|
||||
"mongoose-autopopulate": "^0.6.0",
|
||||
"node-fetch": "^1.7.3",
|
||||
"oauth-1.0a": "github:rinsuki/oauth-1.0a",
|
||||
"parse-link-header": "^1.0.1",
|
||||
"rndstr": "^1.0.0"
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ export interface APIUser {
|
||||
createdAt: string
|
||||
name: string
|
||||
acct: string
|
||||
acctDisplay: string
|
||||
avatarUrl: string
|
||||
url: string
|
||||
allAnon: boolean
|
||||
@ -26,4 +27,5 @@ export interface APIUser {
|
||||
description: string | undefined
|
||||
hostName: string
|
||||
pushbulletEnabled: boolean
|
||||
isTwitter: boolean
|
||||
}
|
@ -26,7 +26,7 @@ export default class Header extends React.Component<{}, State> {
|
||||
<Nav className="mr-auto" navbar>
|
||||
<NavItem>
|
||||
{me
|
||||
? <NavLink to="/my">@{me.acct}<QuestionRemaining/></NavLink>
|
||||
? <NavLink to="/my">@{me.acctDisplay}<QuestionRemaining/></NavLink>
|
||||
: <NavLink to="/login">ログイン</NavLink>}
|
||||
</NavItem>
|
||||
</Nav>
|
||||
|
@ -101,7 +101,7 @@ export default class PageMySettings extends React.Component<{},State> {
|
||||
allDeleteQuestions() {
|
||||
if (!me) return
|
||||
const rand = Math.floor(Math.random() * 9) + 1
|
||||
if (prompt(`あなた(@${me.acct})あてに来た質問を「回答済みのものも含めて全て」削除します。
|
||||
if (prompt(`あなた(@${me.acctDisplay})あてに来た質問を「回答済みのものも含めて全て」削除します。
|
||||
|
||||
確認のために「${rand}」を下に入力してください(数字だけ入力してください)`, "") != rand.toString()) return
|
||||
apiFetch("/api/web/questions/all_delete", {
|
||||
|
@ -38,7 +38,7 @@ export default class PageUserIndex extends React.Component<Props,State> {
|
||||
const { user } = this.state
|
||||
if (!user) return <Loading/>
|
||||
return <div>
|
||||
<Title>{user.name} @{user.acct} さんの{user.questionBoxName}</Title>
|
||||
<Title>{user.name} @{user.acctDisplay} さんの{user.questionBoxName}</Title>
|
||||
<Jumbotron><div style={{textAlign: "center"}}>
|
||||
<img src={user.avatarUrl} style={{maxWidth: "8em", height: "8em"}}/>
|
||||
<h1>{user.name}</h1>
|
||||
@ -46,7 +46,7 @@ export default class PageUserIndex extends React.Component<Props,State> {
|
||||
さんの{user.questionBoxName || "質問箱"}
|
||||
<a href={user.url || `https://${user.hostName}/@${user.acct.split("@")[0]}`}
|
||||
rel="nofollow">
|
||||
Mastodonのプロフィール
|
||||
{user.isTwitter ? "Twitter" : "Mastodon"}のプロフィール
|
||||
</a>
|
||||
</p>
|
||||
<p>{user.description}</p>
|
||||
|
@ -44,7 +44,7 @@ export default class Question extends React.Component<Props, State> {
|
||||
{ this.state.nsfwGuard && <div className="nsfw-guard" onClick={this.nsfwGuardClick.bind(this)}>
|
||||
<div>
|
||||
<div>閲覧注意</div>
|
||||
{ !this.props.hideAnswerUser && <div>回答者: @{this.props.user.acct}</div>}
|
||||
{ !this.props.hideAnswerUser && <div>回答者: @{this.props.user.acctDisplay}</div>}
|
||||
<div>クリック/タップで表示</div>
|
||||
</div>
|
||||
</div> }
|
||||
|
@ -9,7 +9,7 @@ export default class UserLink extends React.Component<Props> {
|
||||
render() {
|
||||
return <Link to={`/@${this.props.acct}`}>
|
||||
{this.props.name}
|
||||
<span className="text-muted"> @{this.props.acct}</span>
|
||||
<span className="text-muted"> @{this.props.acctDisplay}</span>
|
||||
</Link>
|
||||
}
|
||||
}
|
@ -3,80 +3,178 @@ import { MastodonApp, User } from "../../db/index"
|
||||
import fetch from "node-fetch"
|
||||
import { BASE_URL } from "../../config";
|
||||
import rndstr from "rndstr"
|
||||
import requestOAuth from "../../utils/requestOAuth";
|
||||
import QueryStringUtils from "../../utils/queryString"
|
||||
import twitterClient from "../../utils/twitterClient"
|
||||
|
||||
var router = new Router
|
||||
|
||||
router.post("/get_url", async ctx => {
|
||||
const hostName = ctx.request.body.fields.instance
|
||||
.replace(/.*@/, "")
|
||||
.replace(/.*@/, "").toLowerCase()
|
||||
if(~hostName.indexOf("/")) return ctx.reject(400, "not use slash in hostname")
|
||||
const redirectUri = BASE_URL+"/api/web/oauth/redirect"
|
||||
var app = await MastodonApp.findOne({hostName, appBaseUrl: BASE_URL, redirectUri})
|
||||
if (!app) {
|
||||
const res = await fetch("https://"+hostName+"/api/v1/apps", {
|
||||
var url = ""
|
||||
if (hostName !== "twitter.com") { // Mastodon
|
||||
var app = await MastodonApp.findOne({hostName, appBaseUrl: BASE_URL, redirectUri})
|
||||
if (!app) {
|
||||
const res = await fetch("https://"+hostName+"/api/v1/apps", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
client_name: "Quesdon",
|
||||
redirect_uris: redirectUri,
|
||||
scopes: "read write",
|
||||
website: BASE_URL
|
||||
}),
|
||||
headers: {"Content-Type": "application/json"}
|
||||
}).then(r => r.json())
|
||||
app = new MastodonApp
|
||||
app.clientId = res.client_id
|
||||
app.clientSecret = res.client_secret
|
||||
app.hostName = hostName
|
||||
app.appBaseUrl = BASE_URL
|
||||
app.redirectUri = redirectUri
|
||||
await app.save()
|
||||
ctx.session!.loginState = rndstr()+"_"+app.id
|
||||
url = `https://${app.hostName}/oauth/authorize?client_id=${app.clientId}&scope=read+write&redirect_uri=${redirectUri}&response_type=code&state=${ctx.session!.loginState}`
|
||||
}
|
||||
} else { // Twitter
|
||||
ctx.session!.loginState = "twitter"
|
||||
const { TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET } = process.env
|
||||
if (TWITTER_CONSUMER_KEY == null || TWITTER_CONSUMER_SECRET == null) {
|
||||
ctx.throw(500, "twitter not supported in this server.")
|
||||
}
|
||||
const requestTokenRes = await requestOAuth(twitterClient, {
|
||||
url: "https://api.twitter.com/oauth/request_token",
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
client_name: "Quesdon",
|
||||
redirect_uris: redirectUri,
|
||||
scopes: "read write",
|
||||
website: BASE_URL
|
||||
}),
|
||||
headers: {"Content-Type": "application/json"}
|
||||
}).then(r => r.json())
|
||||
app = new MastodonApp
|
||||
app.clientId = res.client_id
|
||||
app.clientSecret = res.client_secret
|
||||
app.hostName = hostName
|
||||
app.appBaseUrl = BASE_URL
|
||||
app.redirectUri = redirectUri
|
||||
await app.save()
|
||||
data: {}
|
||||
}).then(r => r.text()).then(r => QueryStringUtils.decode(r))
|
||||
console.log(requestTokenRes)
|
||||
var requestToken = {
|
||||
token: requestTokenRes.oauth_token,
|
||||
secret: requestTokenRes.oauth_token_secret
|
||||
}
|
||||
ctx.session!.twitterOAuth = requestToken
|
||||
url = `https://twitter.com/oauth/authorize?oauth_token=${requestToken.token}`
|
||||
}
|
||||
ctx.session!.loginState = rndstr()+"_"+app.id
|
||||
ctx.body = {
|
||||
url: `https://${app.hostName}/oauth/authorize?client_id=${app.clientId}&scope=read+write&redirect_uri=${redirectUri}&response_type=code&state=${ctx.session!.loginState}`
|
||||
url
|
||||
}
|
||||
})
|
||||
|
||||
router.get("/redirect", async ctx => {
|
||||
console.log(ctx.session)
|
||||
if (ctx.query.state != ctx.session!.loginState) {
|
||||
ctx.redirect("/login?error=invalid_state")
|
||||
return
|
||||
var profile: {
|
||||
id: string
|
||||
name: string
|
||||
screenName: string
|
||||
avatarUrl: string
|
||||
accessToken: string
|
||||
hostName: string
|
||||
url: string
|
||||
acct: string
|
||||
}
|
||||
const app = await MastodonApp.findById(ctx.session!.loginState.split("_")[1])
|
||||
if (app == null) {
|
||||
ctx.redirect("/login?error=app_notfound")
|
||||
return
|
||||
if (ctx.session!.loginState != "twitter") {
|
||||
if (ctx.query.state != ctx.session!.loginState) {
|
||||
ctx.redirect("/login?error=invalid_state")
|
||||
return
|
||||
}
|
||||
const app = await MastodonApp.findById(ctx.session!.loginState.split("_")[1])
|
||||
if (app == null) {
|
||||
ctx.redirect("/login?error=app_notfound")
|
||||
return
|
||||
}
|
||||
const res = await fetch("https://"+app.hostName+"/oauth/token", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: app.redirectUri,
|
||||
client_id: app.clientId,
|
||||
client_secret: app.clientSecret,
|
||||
code: ctx.query.code,
|
||||
state: ctx.query.state
|
||||
}),
|
||||
headers: {"Content-Type": "application/json"}
|
||||
}).then(r => r.json())
|
||||
console.log(res)
|
||||
const myProfile = await fetch("https://"+app.hostName+"/api/v1/accounts/verify_credentials", {
|
||||
headers: {"Authorization": "Bearer "+res.access_token}
|
||||
}).then(r => r.json())
|
||||
console.log(myProfile)
|
||||
profile = {
|
||||
id: myProfile.id,
|
||||
name: myProfile.display_name || myProfile.username,
|
||||
screenName: myProfile.username,
|
||||
hostName: app.hostName,
|
||||
avatarUrl: myProfile.avatar_static,
|
||||
accessToken: res.access_token,
|
||||
url: myProfile.url,
|
||||
acct: myProfile.display_name + "@" + app.hostName,
|
||||
}
|
||||
} else { // twitter
|
||||
const requestToken: {
|
||||
token: string
|
||||
secret: string
|
||||
} | undefined = ctx.session!.twitterOAuth
|
||||
if (!requestToken) return ctx.redirect("/login?error=no_request_token")
|
||||
if (requestToken.token != ctx.query.oauth_token) return ctx.redirect("/login?error=invalid_request_token")
|
||||
var accessToken
|
||||
try {
|
||||
const accessTokenRes = await requestOAuth(twitterClient, {
|
||||
url: "https://api.twitter.com/oauth/access_token",
|
||||
method: "POST",
|
||||
data: {oauth_verifier: ctx.query.oauth_verifier}
|
||||
}, {
|
||||
key: requestToken.token,
|
||||
secret: requestToken.secret,
|
||||
}).then(r => r.text()).then(r => QueryStringUtils.decode(r))
|
||||
accessToken = {
|
||||
key: accessTokenRes.oauth_token,
|
||||
secret: accessTokenRes.oauth_token_secret
|
||||
}
|
||||
} catch(e) {
|
||||
return ctx.redirect("/login?error=failed_access_token_fetch")
|
||||
}
|
||||
var a
|
||||
try {
|
||||
a = await requestOAuth(twitterClient, {
|
||||
url: "https://api.twitter.com/1.1/account/verify_credentials.json",
|
||||
method: "GET",
|
||||
data: {}
|
||||
}, accessToken).then(r => r.json())
|
||||
} catch(e) {
|
||||
return ctx.redirect("/login?error=failed_user_profile_fetch")
|
||||
}
|
||||
profile = {
|
||||
id: a.id_str,
|
||||
name: a.name,
|
||||
screenName: a.screen_name,
|
||||
hostName: "twitter.com",
|
||||
avatarUrl: a.profile_image_url_https.replace("_normal.", "_400x400."),
|
||||
accessToken: accessToken.key+":"+accessToken.secret,
|
||||
url: "https://twitter.com/"+a.screen_name,
|
||||
acct: a.screen_name+":"+a.id_str+"@twitter.com",
|
||||
}
|
||||
}
|
||||
if (!profile) return
|
||||
const acct = profile.acct
|
||||
var user
|
||||
if (profile.hostName != "twitter.com") { // Mastodon
|
||||
user = await User.findOne({acctLower: acct.toLowerCase()})
|
||||
} else {
|
||||
user = await User.findOne({upstreamId: profile.id, hostName: profile.hostName})
|
||||
}
|
||||
const res = await fetch("https://"+app.hostName+"/oauth/token", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: app.redirectUri,
|
||||
client_id: app.clientId,
|
||||
client_secret: app.clientSecret,
|
||||
code: ctx.query.code,
|
||||
state: ctx.query.state
|
||||
}),
|
||||
headers: {"Content-Type": "application/json"}
|
||||
}).then(r => r.json())
|
||||
console.log(res)
|
||||
const myProfile = await fetch("https://"+app.hostName+"/api/v1/accounts/verify_credentials", {
|
||||
headers: {"Authorization": "Bearer "+res.access_token}
|
||||
}).then(r => r.json())
|
||||
console.log(myProfile)
|
||||
const acct = myProfile.username + "@" + app.hostName
|
||||
var user = await User.findOne({acctLower: acct.toLowerCase()})
|
||||
if (user == null) {
|
||||
user = new User
|
||||
user.acct = acct
|
||||
user.acctLower = acct.toLowerCase()
|
||||
user.app = app
|
||||
}
|
||||
user.name = myProfile.display_name || myProfile.username
|
||||
user.avatarUrl = myProfile.avatar_static
|
||||
user.accessToken = res.access_token
|
||||
user.url = myProfile.url
|
||||
user.acct = acct
|
||||
user.acctLower = acct.toLowerCase()
|
||||
user.name = profile.name
|
||||
user.avatarUrl = profile.avatarUrl
|
||||
user.accessToken = profile.accessToken
|
||||
user.hostName = profile.hostName
|
||||
user.url = profile.url
|
||||
user.upstreamId = profile.id
|
||||
await user.save()
|
||||
ctx.session!.user = user.id
|
||||
ctx.redirect("/my")
|
||||
|
@ -3,6 +3,9 @@ import * as mongoose from "mongoose"
|
||||
import { User, Question, IMastodonApp, QuestionLike, IUser } from "../../db/index";
|
||||
import fetch from "node-fetch";
|
||||
import { BASE_URL } from "../../config";
|
||||
import twitterClient from "../../utils/twitterClient"
|
||||
import requestOAuth from "../../utils/requestOAuth";
|
||||
import cutText from "../../utils/cutText";
|
||||
|
||||
var router = new Router
|
||||
|
||||
@ -50,26 +53,45 @@ router.post("/:id/answer", async ctx => {
|
||||
ctx.body = {status: "ok"}
|
||||
const user = await User.findById(ctx.session!.user)
|
||||
if (!~["public","unlisted","private"].indexOf(ctx.request.body.fields.visibility)) return
|
||||
var body = {
|
||||
spoiler_text: "Q. "+question.question + " #quesdon",
|
||||
status: "A. " + (question.answer!.length > 200 ? question.answer!.substring(0,200) + "...(続きはリンク先で)" : question.answer) + "\n#quesdon "+BASE_URL+"/@"+user!.acct+"/questions/"+question.id,
|
||||
visibility: ctx.request.body.fields.visibility
|
||||
}
|
||||
if (question.questionUser) {
|
||||
body.status = "質問者: @"+question.questionUser.acct + "\n" + body.status
|
||||
}
|
||||
if (question.isNSFW) {
|
||||
body.status = "Q. "+question.question + "\n" + body.status
|
||||
body.spoiler_text = "⚠ この質問は回答者がNSFWであると申告しています #quesdon"
|
||||
}
|
||||
fetch("https://"+user!.acct.split("@")[1]+"/api/v1/statuses", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
Authorization: "Bearer "+user!.accessToken,
|
||||
"Content-Type": "application/json"
|
||||
if (!user) return
|
||||
const isTwitter = user.hostName == "twitter.com"
|
||||
const answerCharMax = isTwitter ? (110-question.question.length) : 200
|
||||
const answerUrl = "https://example.com"+"/@"+user!.acct+"/questions/"+question.id
|
||||
if (!isTwitter) { // Mastodon
|
||||
var body = {
|
||||
spoiler_text: "Q. "+question.question + " #quesdon",
|
||||
status: "A. " + (question.answer!.length > 200 ? question.answer!.substring(0,200) + "...(続きはリンク先で)" : question.answer) + "\n#quesdon "+BASE_URL+"/@"+user!.acct+"/questions/"+question.id,
|
||||
visibility: ctx.request.body.fields.visibility
|
||||
}
|
||||
})
|
||||
if (question.questionUser) {
|
||||
body.status = "質問者: @"+question.questionUser.acct + "\n" + body.status
|
||||
}
|
||||
if (question.isNSFW) {
|
||||
body.status = "Q. "+question.question + "\n" + body.status
|
||||
body.spoiler_text = "⚠ この質問は回答者がNSFWであると申告しています #quesdon"
|
||||
}
|
||||
fetch("https://"+user!.acct.split("@")[1]+"/api/v1/statuses", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
Authorization: "Bearer "+user!.accessToken,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const strQ = cutText(question.question, 60)
|
||||
const strA = cutText(question.answer!, 120-strQ.length)
|
||||
const [key, secret] = user.accessToken.split(":")
|
||||
const body = "Q. "+strQ+"\nA. "+strA+"\n#quesdon "+answerUrl
|
||||
console.log(body, body.length)
|
||||
requestOAuth(twitterClient, {
|
||||
url: "https://api.twitter.com/1.1/statuses/update.json",
|
||||
method: "POST",
|
||||
data: {status: body}
|
||||
}, {
|
||||
key, secret
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
router.post("/:id/delete", async ctx => {
|
||||
|
@ -5,7 +5,7 @@ import setTransformer from "../utils/setTransformer"
|
||||
var schema = new mongoose.Schema({
|
||||
acct: {type: String, required: true},
|
||||
acctLower: {type: String, required: true, unique: true},
|
||||
app: {type: mongoose.Schema.Types.ObjectId, required: true, ref: "mastodon_apps"},
|
||||
app: {type: mongoose.Schema.Types.ObjectId, ref: "mastodon_apps"},
|
||||
name: {type: String, required: true},
|
||||
avatarUrl: {type: String, required: true},
|
||||
accessToken: {type: String, required: true},
|
||||
@ -14,6 +14,8 @@ var schema = new mongoose.Schema({
|
||||
questionBoxName: {type: String, default: "質問箱"},
|
||||
pushbulletAccessToken: {type: String},
|
||||
allAnon: {type: Boolean, default: false},
|
||||
upstreamId: {type: String},
|
||||
hostName: {type: String},
|
||||
}, {
|
||||
timestamps: true
|
||||
})
|
||||
@ -24,7 +26,10 @@ setTransformer(schema, (doc: IUser, ret: any) => {
|
||||
delete ret.accessToken
|
||||
delete ret.acctLower
|
||||
delete ret.pushbulletAccessToken
|
||||
delete ret.upstreamId
|
||||
ret.isTwitter = ret.hostName == "twitter.com"
|
||||
ret.pushbulletEnabled = !!doc.pushbulletAccessToken
|
||||
ret.acctDisplay = ret.acct.replace(/:[0-9]+@/, "@")
|
||||
return ret
|
||||
})
|
||||
|
||||
@ -40,6 +45,8 @@ export interface IUser extends mongoose.Document {
|
||||
questionBoxName: string
|
||||
pushbulletAccessToken: string | null
|
||||
allAnon: boolean
|
||||
upstreamId: string | null
|
||||
hostName: string | null
|
||||
}
|
||||
|
||||
export default mongoose.model("users", schema) as mongoose.Model<IUser>
|
7
src/server/utils/cutText.ts
Normal file
7
src/server/utils/cutText.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export default function cutText(s: string, l: number) {
|
||||
if (s.length <= l) {
|
||||
return s
|
||||
} else {
|
||||
return s.slice(0, l-1)+"…"
|
||||
}
|
||||
}
|
23
src/server/utils/queryString.ts
Normal file
23
src/server/utils/queryString.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import * as OAuth from "oauth-1.0a"
|
||||
|
||||
export default new class QueryStringUtils{
|
||||
decode(query: string) {
|
||||
var res: {[key: string]: string} = {}
|
||||
query.split("&").map(q => {
|
||||
const splitedQuery = q.split("=")
|
||||
if (splitedQuery.length < 2) {
|
||||
return
|
||||
}
|
||||
const name = splitedQuery[0]
|
||||
const value = splitedQuery.slice(1).join("=")
|
||||
res[name] = decodeURIComponent(value)
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
encode(params: {[key: string]: any}) {
|
||||
return Object.keys(params).map(key => {
|
||||
return encodeURIComponent(key) + "=" + OAuth.prototype.percentEncode(params[key])
|
||||
}).join("&")
|
||||
}
|
||||
}()
|
25
src/server/utils/requestOAuth.ts
Normal file
25
src/server/utils/requestOAuth.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import * as OAuth from "oauth-1.0a"
|
||||
import fetch, { Response } from "node-fetch"
|
||||
import QueryStringUtils from "./queryString";
|
||||
|
||||
export default function request(oauth: OAuth, options: OAuth.RequestOptions, token: OAuth.Token | undefined = undefined) {
|
||||
var opt = {
|
||||
url: options.url,
|
||||
method: options.method,
|
||||
body: QueryStringUtils.encode(options.data),
|
||||
headers: {...oauth.toHeader(oauth.authorize(options, token)), "Content-Type": "application/x-www-form-urlencoded"}
|
||||
}
|
||||
if (options.method == "GET") {
|
||||
opt.url += "?" + opt.body
|
||||
delete opt.body
|
||||
delete opt.headers["Content-Type"]
|
||||
}
|
||||
return fetch(opt.url, opt).then(r => {
|
||||
if (!r.ok) {
|
||||
return r.text().then(r => {
|
||||
throw "API Error: "+r
|
||||
}) as Promise<Response>
|
||||
}
|
||||
return Promise.resolve(r)
|
||||
})
|
||||
}
|
16
src/server/utils/twitterClient.ts
Normal file
16
src/server/utils/twitterClient.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import * as OAuth from "oauth-1.0a"
|
||||
import * as crypto from "crypto"
|
||||
|
||||
const twitterClient = new OAuth({
|
||||
consumer: {
|
||||
key: process.env.TWITTER_CONSUMER_KEY!,
|
||||
secret: process.env.TWITTER_CONSUMER_SECRET!,
|
||||
},
|
||||
signature_method: "HMAC-SHA1",
|
||||
hash_function(base_string, key) {
|
||||
return crypto.createHmac("sha1", key).update(base_string).digest("base64")
|
||||
},
|
||||
realm: ""
|
||||
})
|
||||
|
||||
export default twitterClient
|
Loading…
Reference in New Issue
Block a user