The code powering m.abunchtell.com https://m.abunchtell.com
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 
 
 

163 Zeilen
4.6 KiB

  1. import dotenv from 'dotenv'
  2. import express from 'express'
  3. import redis from 'redis'
  4. import pg from 'pg'
  5. import log from 'npmlog'
  6. const env = process.env.NODE_ENV || 'development'
  7. dotenv.config({
  8. path: env === 'production' ? '.env.production' : '.env'
  9. })
  10. const pgConfigs = {
  11. development: {
  12. database: 'mastodon_development',
  13. host: '/var/run/postgresql',
  14. max: 10
  15. },
  16. production: {
  17. user: process.env.DB_USER || 'mastodon',
  18. password: process.env.DB_PASS || '',
  19. database: process.env.DB_NAME || 'mastodon_production',
  20. host: process.env.DB_HOST || 'localhost',
  21. port: process.env.DB_PORT || 5432,
  22. max: 10
  23. }
  24. }
  25. const app = express()
  26. const pgPool = new pg.Pool(pgConfigs[env])
  27. const allowCrossDomain = (req, res, next) => {
  28. res.header('Access-Control-Allow-Origin', '*')
  29. res.header('Access-Control-Allow-Headers', 'Authorization, Accept, Cache-Control')
  30. res.header('Access-Control-Allow-Methods', 'GET, OPTIONS')
  31. next()
  32. }
  33. const authenticationMiddleware = (req, res, next) => {
  34. if (req.method === 'OPTIONS') {
  35. return next()
  36. }
  37. const authorization = req.get('Authorization')
  38. if (!authorization) {
  39. const err = new Error('Missing access token')
  40. err.statusCode = 401
  41. return next(err)
  42. }
  43. const token = authorization.replace(/^Bearer /, '')
  44. pgPool.connect((err, client, done) => {
  45. if (err) {
  46. return next(err)
  47. }
  48. client.query('SELECT oauth_access_tokens.resource_owner_id, users.account_id FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id WHERE oauth_access_tokens.token = $1 LIMIT 1', [token], (err, result) => {
  49. done()
  50. if (err) {
  51. return next(err)
  52. }
  53. if (result.rows.length === 0) {
  54. err = new Error('Invalid access token')
  55. err.statusCode = 401
  56. return next(err)
  57. }
  58. req.accountId = result.rows[0].account_id
  59. next()
  60. })
  61. })
  62. }
  63. const errorMiddleware = (err, req, res, next) => {
  64. log.error(err)
  65. res.writeHead(err.statusCode || 500, { 'Content-Type': 'application/json' })
  66. res.end(JSON.stringify({ error: err.statusCode ? `${err}` : 'An unexpected error occured' }))
  67. }
  68. const placeholders = (arr, shift = 0) => arr.map((_, i) => `$${i + 1 + shift}`).join(', ');
  69. const streamFrom = (id, req, res, needsFiltering = false) => {
  70. log.verbose(`Starting stream from ${id} for ${req.accountId}`)
  71. res.setHeader('Content-Type', 'text/event-stream')
  72. res.setHeader('Transfer-Encoding', 'chunked')
  73. const redisClient = redis.createClient({
  74. host: process.env.REDIS_HOST || '127.0.0.1',
  75. port: process.env.REDIS_PORT || 6379,
  76. password: process.env.REDIS_PASSWORD
  77. })
  78. redisClient.on('message', (channel, message) => {
  79. const { event, payload } = JSON.parse(message)
  80. // Only messages that may require filtering are statuses, since notifications
  81. // are already personalized and deletes do not matter
  82. if (needsFiltering && event === 'update') {
  83. pgPool.connect((err, client, done) => {
  84. if (err) {
  85. log.error(err)
  86. return
  87. }
  88. const unpackedPayload = JSON.parse(payload)
  89. const targetAccountIds = [unpackedPayload.account.id].concat(unpackedPayload.mentions.map(item => item.id)).concat(unpackedPayload.reblog ? [unpackedPayload.reblog.account.id] : [])
  90. client.query(`SELECT target_account_id FROM blocks WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 1)})`, [req.accountId].concat(targetAccountIds), (err, result) => {
  91. done()
  92. if (err) {
  93. log.error(err)
  94. return
  95. }
  96. if (result.rows.length > 0) {
  97. return
  98. }
  99. res.write(`event: ${event}\n`)
  100. res.write(`data: ${payload}\n\n`)
  101. })
  102. })
  103. } else {
  104. res.write(`event: ${event}\n`)
  105. res.write(`data: ${payload}\n\n`)
  106. }
  107. })
  108. const heartbeat = setInterval(() => res.write(':thump\n'), 15000)
  109. req.on('close', () => {
  110. log.verbose(`Ending stream from ${id} for ${req.accountId}`)
  111. clearInterval(heartbeat)
  112. redisClient.quit()
  113. })
  114. redisClient.subscribe(id)
  115. }
  116. app.use(allowCrossDomain)
  117. app.use(authenticationMiddleware)
  118. app.use(errorMiddleware)
  119. app.get('/api/v1/streaming/user', (req, res) => streamFrom(`timeline:${req.accountId}`, req, res))
  120. app.get('/api/v1/streaming/public', (req, res) => streamFrom('timeline:public', req, res, true))
  121. app.get('/api/v1/streaming/hashtag', (req, res) => streamFrom(`timeline:hashtag:${req.params.tag}`, req, res, true))
  122. log.level = 'verbose'
  123. log.info(`Starting HTTP server on port ${process.env.PORT || 4000}`)
  124. app.listen(process.env.PORT || 4000)