2019-04-09 13:59:33 +02:00
//
// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package main
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"database/sql"
"fmt"
2019-04-15 16:56:43 +02:00
"html"
2019-04-09 13:59:33 +02:00
"html/template"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"io"
"log"
2019-04-16 02:52:24 +02:00
notrand "math/rand"
2019-04-09 13:59:33 +02:00
"net/http"
"os"
"sort"
"strings"
"sync"
"time"
"github.com/gorilla/mux"
)
type UserInfo struct {
UserID int64
Username string
}
type WhatAbout struct {
ID int64
Name string
Display string
About string
Key string
URL string
}
2019-04-13 19:58:42 +02:00
type Honk struct {
ID int64
UserID int64
Username string
What string
Honker string
XID string
RID string
Date time . Time
URL string
Noise string
2019-04-19 17:08:22 +02:00
Convoy string
2019-04-13 19:58:42 +02:00
Audience [ ] string
HTML template . HTML
Donks [ ] * Donk
2019-04-09 13:59:33 +02:00
}
2019-04-13 19:58:42 +02:00
type Donk struct {
FileID int64
XID string
Name string
URL string
Media string
Content [ ] byte
2019-04-09 13:59:33 +02:00
}
2019-04-13 19:58:42 +02:00
type Honker struct {
ID int64
UserID int64
Name string
XID string
Flavor string
2019-04-09 13:59:33 +02:00
}
2019-04-13 19:58:42 +02:00
var serverName string
var iconName = "icon.png"
var readviews * Template
2019-04-09 13:59:33 +02:00
func getInfo ( r * http . Request ) map [ string ] interface { } {
templinfo := make ( map [ string ] interface { } )
2019-04-12 19:12:18 +02:00
templinfo [ "StyleParam" ] = getstyleparam ( "views/style.css" )
templinfo [ "LocalStyleParam" ] = getstyleparam ( "views/local.css" )
2019-04-09 13:59:33 +02:00
templinfo [ "ServerName" ] = serverName
templinfo [ "IconName" ] = iconName
templinfo [ "UserInfo" ] = GetUserInfo ( r )
templinfo [ "LogoutCSRF" ] = GetCSRF ( "logout" , r )
return templinfo
}
func homepage ( w http . ResponseWriter , r * http . Request ) {
templinfo := getInfo ( r )
u := GetUserInfo ( r )
2019-04-14 04:57:16 +02:00
var honks [ ] * Honk
2019-04-09 13:59:33 +02:00
if u != nil {
2019-04-14 04:57:16 +02:00
honks = gethonksforuser ( u . UserID )
2019-04-09 13:59:33 +02:00
templinfo [ "HonkCSRF" ] = GetCSRF ( "honkhonk" , r )
2019-04-14 04:57:16 +02:00
} else {
honks = gethonks ( )
2019-04-09 13:59:33 +02:00
}
sort . Slice ( honks , func ( i , j int ) bool {
return honks [ i ] . Date . After ( honks [ j ] . Date )
} )
2019-04-11 01:25:32 +02:00
var modtime time . Time
if len ( honks ) > 0 {
modtime = honks [ 0 ] . Date
}
2019-04-11 16:05:37 +02:00
debug := false
getconfig ( "debug" , & debug )
2019-04-11 01:25:32 +02:00
imh := r . Header . Get ( "If-Modified-Since" )
2019-04-11 16:05:37 +02:00
if ! debug && imh != "" && ! modtime . IsZero ( ) {
2019-04-11 01:25:32 +02:00
ifmod , err := time . Parse ( http . TimeFormat , imh )
if err == nil && ! modtime . After ( ifmod ) {
w . WriteHeader ( http . StatusNotModified )
return
}
}
2019-04-17 04:26:17 +02:00
reverbolate ( honks )
2019-04-11 01:25:32 +02:00
2019-04-09 13:59:33 +02:00
msg := "Things happen."
getconfig ( "servermsg" , & msg )
templinfo [ "Honks" ] = honks
templinfo [ "ShowRSS" ] = true
templinfo [ "ServerMessage" ] = msg
2019-04-17 04:26:17 +02:00
if u == nil {
w . Header ( ) . Set ( "Cache-Control" , "max-age=60" )
} else {
w . Header ( ) . Set ( "Cache-Control" , "max-age=0" )
}
2019-04-11 01:25:32 +02:00
w . Header ( ) . Set ( "Last-Modified" , modtime . Format ( http . TimeFormat ) )
2019-04-09 13:59:33 +02:00
err := readviews . ExecuteTemplate ( w , "homepage.html" , templinfo )
if err != nil {
log . Print ( err )
}
}
func showrss ( w http . ResponseWriter , r * http . Request ) {
name := mux . Vars ( r ) [ "name" ]
2019-04-14 04:57:16 +02:00
var honks [ ] * Honk
if name != "" {
honks = gethonksbyuser ( name )
} else {
honks = gethonks ( )
}
2019-04-09 13:59:33 +02:00
sort . Slice ( honks , func ( i , j int ) bool {
return honks [ i ] . Date . After ( honks [ j ] . Date )
} )
reverbolate ( honks )
home := fmt . Sprintf ( "https://%s/" , serverName )
base := home
if name != "" {
home += "u/" + name
name += " "
}
feed := RssFeed {
Title : name + "honk" ,
Link : home ,
Description : name + "honk rss" ,
FeedImage : & RssFeedImage {
URL : base + "icon.png" ,
Title : name + "honk rss" ,
Link : home ,
} ,
}
var modtime time . Time
past := time . Now ( ) . UTC ( ) . Add ( - 3 * 24 * time . Hour )
for _ , honk := range honks {
if honk . Date . Before ( past ) {
break
}
2019-04-15 16:56:43 +02:00
desc := string ( honk . HTML )
for _ , d := range honk . Donks {
desc += fmt . Sprintf ( ` <p><a href="%sd/%s">Attachment: %s</a> ` ,
base , d . XID , html . EscapeString ( d . Name ) )
2019-04-09 13:59:33 +02:00
}
2019-04-15 16:56:43 +02:00
2019-04-09 13:59:33 +02:00
feed . Items = append ( feed . Items , & RssItem {
Title : fmt . Sprintf ( "%s %s %s" , honk . Username , honk . What , honk . XID ) ,
2019-04-15 16:56:43 +02:00
Description : RssCData { desc } ,
2019-04-09 13:59:33 +02:00
Link : honk . URL ,
PubDate : honk . Date . Format ( time . RFC1123 ) ,
} )
if honk . Date . After ( modtime ) {
modtime = honk . Date
}
}
w . Header ( ) . Set ( "Cache-Control" , "max-age=300" )
w . Header ( ) . Set ( "Last-Modified" , modtime . Format ( http . TimeFormat ) )
err := feed . Write ( w )
if err != nil {
log . Printf ( "error writing rss: %s" , err )
}
}
func butwhatabout ( name string ) ( * WhatAbout , error ) {
row := stmtWhatAbout . QueryRow ( name )
var user WhatAbout
err := row . Scan ( & user . ID , & user . Name , & user . Display , & user . About , & user . Key )
user . URL = fmt . Sprintf ( "https://%s/u/%s" , serverName , user . Name )
return & user , err
}
func crappola ( j map [ string ] interface { } ) bool {
t , _ := jsongetstring ( j , "type" )
a , _ := jsongetstring ( j , "actor" )
o , _ := jsongetstring ( j , "object" )
if t == "Delete" && a == o {
log . Printf ( "crappola from %s" , a )
return true
}
return false
}
2019-04-12 18:32:07 +02:00
func ping ( user * WhatAbout , who string ) {
2019-04-16 20:40:23 +02:00
box , err := getboxes ( who )
2019-04-12 18:32:07 +02:00
if err != nil {
log . Printf ( "no inbox for ping: %s" , err )
return
}
j := NewJunk ( )
j [ "@context" ] = itiswhatitis
j [ "type" ] = "Ping"
j [ "id" ] = user . URL + "/ping/" + xfiltrate ( )
j [ "actor" ] = user . URL
j [ "to" ] = who
2019-04-14 16:06:26 +02:00
keyname , key := ziggy ( user . Name )
2019-04-16 20:40:23 +02:00
err = PostJunk ( keyname , key , box . In , j )
2019-04-12 18:32:07 +02:00
if err != nil {
log . Printf ( "can't send ping: %s" , err )
return
}
log . Printf ( "sent ping to %s: %s" , who , j [ "id" ] )
}
func pong ( user * WhatAbout , who string , obj string ) {
2019-04-16 20:40:23 +02:00
box , err := getboxes ( who )
2019-04-12 18:32:07 +02:00
if err != nil {
log . Printf ( "no inbox for pong %s : %s" , who , err )
return
}
j := NewJunk ( )
j [ "@context" ] = itiswhatitis
j [ "type" ] = "Pong"
j [ "id" ] = user . URL + "/pong/" + xfiltrate ( )
j [ "actor" ] = user . URL
j [ "to" ] = who
j [ "object" ] = obj
2019-04-14 16:06:26 +02:00
keyname , key := ziggy ( user . Name )
2019-04-16 20:40:23 +02:00
err = PostJunk ( keyname , key , box . In , j )
2019-04-12 18:32:07 +02:00
if err != nil {
log . Printf ( "can't send pong: %s" , err )
return
}
}
2019-04-09 13:59:33 +02:00
func inbox ( w http . ResponseWriter , r * http . Request ) {
name := mux . Vars ( r ) [ "name" ]
user , err := butwhatabout ( name )
if err != nil {
http . NotFound ( w , r )
return
}
var buf bytes . Buffer
io . Copy ( & buf , r . Body )
payload := buf . Bytes ( )
j , err := ReadJunk ( bytes . NewReader ( payload ) )
if err != nil {
log . Printf ( "bad payload: %s" , err )
io . WriteString ( os . Stdout , "bad payload\n" )
os . Stdout . Write ( payload )
io . WriteString ( os . Stdout , "\n" )
return
}
if crappola ( j ) {
return
}
keyname , err := zag ( r , payload )
if err != nil {
log . Printf ( "inbox message failed signature: %s" , err )
fd , _ := os . OpenFile ( "savedinbox.json" , os . O_CREATE | os . O_WRONLY | os . O_APPEND , 0666 )
io . WriteString ( fd , "bad signature:\n" )
WriteJunk ( fd , j )
io . WriteString ( fd , "\n" )
fd . Close ( )
return
}
2019-04-18 22:39:29 +02:00
what , _ := jsongetstring ( j , "type" )
if what == "Like" {
return
}
2019-04-09 13:59:33 +02:00
who , _ := jsongetstring ( j , "actor" )
if ! keymatch ( keyname , who ) {
log . Printf ( "keyname actor mismatch: %s <> %s" , keyname , who )
return
}
2019-04-18 22:39:29 +02:00
fd , _ := os . OpenFile ( "savedinbox.json" , os . O_CREATE | os . O_WRONLY | os . O_APPEND , 0666 )
WriteJunk ( fd , j )
io . WriteString ( fd , "\n" )
fd . Close ( )
2019-04-09 13:59:33 +02:00
switch what {
2019-04-12 18:32:07 +02:00
case "Ping" :
obj , _ := jsongetstring ( j , "id" )
log . Printf ( "ping from %s: %s" , who , obj )
pong ( user , who , obj )
case "Pong" :
obj , _ := jsongetstring ( j , "object" )
log . Printf ( "pong from %s: %s" , who , obj )
2019-04-09 13:59:33 +02:00
case "Follow" :
log . Printf ( "updating honker follow: %s" , who )
rubadubdub ( user , j )
case "Accept" :
db := opendatabase ( )
log . Printf ( "updating honker accept: %s" , who )
db . Exec ( "update honkers set flavor = 'sub' where xid = ? and flavor = 'presub'" , who )
case "Undo" :
obj , ok := jsongetmap ( j , "object" )
if ! ok {
log . Printf ( "unknown undo no object" )
} else {
what , _ := jsongetstring ( obj , "type" )
if what != "Follow" {
log . Printf ( "unknown undo: %s" , what )
} else {
log . Printf ( "updating honker undo: %s" , who )
db := opendatabase ( )
db . Exec ( "update honkers set flavor = 'undub' where xid = ? and flavor = 'dub'" , who )
}
}
default :
xonk := xonkxonk ( j )
2019-04-14 16:15:47 +02:00
if xonk != nil && needxonk ( user , xonk ) {
2019-04-09 13:59:33 +02:00
xonk . UserID = user . ID
savexonk ( xonk )
}
}
}
func outbox ( w http . ResponseWriter , r * http . Request ) {
name := mux . Vars ( r ) [ "name" ]
user , err := butwhatabout ( name )
if err != nil {
http . NotFound ( w , r )
return
}
2019-04-14 04:57:16 +02:00
honks := gethonksbyuser ( name )
2019-04-09 13:59:33 +02:00
var jonks [ ] map [ string ] interface { }
for _ , h := range honks {
j , _ := jonkjonk ( user , h )
jonks = append ( jonks , j )
}
j := NewJunk ( )
j [ "@context" ] = itiswhatitis
j [ "id" ] = user . URL + "/outbox"
j [ "type" ] = "OrderedCollection"
j [ "totalItems" ] = len ( jonks )
j [ "orderedItems" ] = jonks
2019-04-17 04:23:50 +02:00
w . Header ( ) . Set ( "Cache-Control" , "max-age=60" )
2019-04-09 13:59:33 +02:00
w . Header ( ) . Set ( "Content-Type" , theonetruename )
WriteJunk ( w , j )
}
func viewuser ( w http . ResponseWriter , r * http . Request ) {
name := mux . Vars ( r ) [ "name" ]
user , err := butwhatabout ( name )
if err != nil {
http . NotFound ( w , r )
return
}
if friendorfoe ( r . Header . Get ( "Accept" ) ) {
j := asjonker ( user )
2019-04-17 04:23:50 +02:00
w . Header ( ) . Set ( "Cache-Control" , "max-age=600" )
2019-04-09 13:59:33 +02:00
w . Header ( ) . Set ( "Content-Type" , theonetruename )
WriteJunk ( w , j )
return
}
2019-04-14 04:57:16 +02:00
honks := gethonksbyuser ( name )
2019-04-09 13:59:33 +02:00
u := GetUserInfo ( r )
honkpage ( w , r , u , user , honks )
}
func viewhonker ( w http . ResponseWriter , r * http . Request ) {
name := mux . Vars ( r ) [ "name" ]
u := GetUserInfo ( r )
honks := gethonksbyhonker ( u . UserID , name )
honkpage ( w , r , nil , nil , honks )
}
func fingerlicker ( w http . ResponseWriter , r * http . Request ) {
orig := r . FormValue ( "resource" )
log . Printf ( "finger lick: %s" , orig )
if strings . HasPrefix ( orig , "acct:" ) {
orig = orig [ 5 : ]
}
name := orig
idx := strings . LastIndexByte ( name , '/' )
if idx != - 1 {
name = name [ idx + 1 : ]
if "https://" + serverName + "/u/" + name != orig {
log . Printf ( "foreign request rejected" )
name = ""
}
} else {
idx = strings . IndexByte ( name , '@' )
if idx != - 1 {
name = name [ : idx ]
if name + "@" + serverName != orig {
log . Printf ( "foreign request rejected" )
name = ""
}
}
}
user , err := butwhatabout ( name )
if err != nil {
http . NotFound ( w , r )
return
}
j := NewJunk ( )
j [ "subject" ] = fmt . Sprintf ( "acct:%s@%s" , user . Name , serverName )
j [ "aliases" ] = [ ] string { user . URL }
var links [ ] map [ string ] interface { }
l := NewJunk ( )
l [ "rel" ] = "self"
l [ "type" ] = ` application/activity+json `
l [ "href" ] = user . URL
links = append ( links , l )
j [ "links" ] = links
2019-04-17 04:23:50 +02:00
w . Header ( ) . Set ( "Cache-Control" , "max-age=3600" )
2019-04-09 13:59:33 +02:00
w . Header ( ) . Set ( "Content-Type" , "application/jrd+json" )
WriteJunk ( w , j )
}
func viewhonk ( w http . ResponseWriter , r * http . Request ) {
name := mux . Vars ( r ) [ "name" ]
xid := mux . Vars ( r ) [ "xid" ]
user , err := butwhatabout ( name )
if err != nil {
http . NotFound ( w , r )
return
}
h := getxonk ( name , xid )
if h == nil {
http . NotFound ( w , r )
return
}
if friendorfoe ( r . Header . Get ( "Accept" ) ) {
_ , j := jonkjonk ( user , h )
j [ "@context" ] = itiswhatitis
2019-04-17 04:23:50 +02:00
w . Header ( ) . Set ( "Cache-Control" , "max-age=3600" )
2019-04-09 13:59:33 +02:00
w . Header ( ) . Set ( "Content-Type" , theonetruename )
WriteJunk ( w , j )
return
}
honkpage ( w , r , nil , nil , [ ] * Honk { h } )
}
func honkpage ( w http . ResponseWriter , r * http . Request , u * UserInfo , user * WhatAbout , honks [ ] * Honk ) {
reverbolate ( honks )
templinfo := getInfo ( r )
if u != nil && u . Username == user . Name {
templinfo [ "UserCSRF" ] = GetCSRF ( "saveuser" , r )
templinfo [ "HonkCSRF" ] = GetCSRF ( "honkhonk" , r )
}
2019-04-17 04:23:50 +02:00
if u == nil {
w . Header ( ) . Set ( "Cache-Control" , "max-age=60" )
}
2019-04-09 13:59:33 +02:00
if user != nil {
templinfo [ "Name" ] = user . Name
whatabout := user . About
templinfo [ "RawWhatAbout" ] = whatabout
whatabout = obfusbreak ( whatabout )
templinfo [ "WhatAbout" ] = cleanstring ( whatabout )
}
templinfo [ "Honks" ] = honks
err := readviews . ExecuteTemplate ( w , "honkpage.html" , templinfo )
if err != nil {
log . Print ( err )
}
}
func saveuser ( w http . ResponseWriter , r * http . Request ) {
whatabout := r . FormValue ( "whatabout" )
u := GetUserInfo ( r )
db := opendatabase ( )
_ , err := db . Exec ( "update users set about = ? where username = ?" , whatabout , u . Username )
if err != nil {
log . Printf ( "error bouting what: %s" , err )
}
http . Redirect ( w , r , "/u/" + u . Username , http . StatusSeeOther )
}
func gethonkers ( userid int64 ) [ ] * Honker {
rows , err := stmtHonkers . Query ( userid )
if err != nil {
log . Printf ( "error querying honkers: %s" , err )
return nil
}
defer rows . Close ( )
var honkers [ ] * Honker
for rows . Next ( ) {
var f Honker
err = rows . Scan ( & f . ID , & f . UserID , & f . Name , & f . XID , & f . Flavor )
if err != nil {
log . Printf ( "error scanning honker: %s" , err )
return nil
}
honkers = append ( honkers , & f )
}
return honkers
}
func getdubs ( userid int64 ) [ ] * Honker {
rows , err := stmtDubbers . Query ( userid )
if err != nil {
log . Printf ( "error querying dubs: %s" , err )
return nil
}
defer rows . Close ( )
var honkers [ ] * Honker
for rows . Next ( ) {
var f Honker
err = rows . Scan ( & f . ID , & f . UserID , & f . Name , & f . XID , & f . Flavor )
if err != nil {
log . Printf ( "error scanning honker: %s" , err )
return nil
}
honkers = append ( honkers , & f )
}
return honkers
}
func getxonk ( name , xid string ) * Honk {
var h Honk
var dt , aud string
row := stmtOneXonk . QueryRow ( xid )
err := row . Scan ( & h . ID , & h . UserID , & h . Username , & h . What , & h . Honker , & h . XID , & h . RID ,
2019-04-19 17:08:22 +02:00
& dt , & h . URL , & aud , & h . Noise , & h . Convoy )
2019-04-09 13:59:33 +02:00
if err != nil {
log . Printf ( "error scanning xonk: %s" , err )
return nil
}
if name != "" && h . Username != name {
log . Printf ( "user xonk mismatch" )
return nil
}
h . Date , _ = time . Parse ( dbtimeformat , dt )
h . Audience = strings . Split ( aud , " " )
donksforhonks ( [ ] * Honk { & h } )
return & h
}
2019-04-14 04:57:16 +02:00
func gethonks ( ) [ ] * Honk {
rows , err := stmtHonks . Query ( )
return getsomehonks ( rows , err )
}
func gethonksbyuser ( name string ) [ ] * Honk {
rows , err := stmtUserHonks . Query ( name )
return getsomehonks ( rows , err )
2019-04-09 13:59:33 +02:00
}
func gethonksforuser ( userid int64 ) [ ] * Honk {
2019-04-14 05:04:17 +02:00
dt := time . Now ( ) . UTC ( ) . Add ( - 2 * 24 * time . Hour )
rows , err := stmtHonksForUser . Query ( userid , dt . Format ( dbtimeformat ) )
2019-04-14 04:57:16 +02:00
return getsomehonks ( rows , err )
2019-04-09 13:59:33 +02:00
}
func gethonksbyhonker ( userid int64 , honker string ) [ ] * Honk {
2019-04-14 04:57:16 +02:00
rows , err := stmtHonksByHonker . Query ( userid , honker )
return getsomehonks ( rows , err )
2019-04-09 13:59:33 +02:00
}
2019-04-14 04:57:16 +02:00
func getsomehonks ( rows * sql . Rows , err error ) [ ] * Honk {
2019-04-09 13:59:33 +02:00
if err != nil {
log . Printf ( "error querying honks: %s" , err )
return nil
}
defer rows . Close ( )
var honks [ ] * Honk
for rows . Next ( ) {
var h Honk
var dt , aud string
err = rows . Scan ( & h . ID , & h . UserID , & h . Username , & h . What , & h . Honker , & h . XID , & h . RID ,
2019-04-19 17:08:22 +02:00
& dt , & h . URL , & aud , & h . Noise , & h . Convoy )
2019-04-09 13:59:33 +02:00
if err != nil {
log . Printf ( "error scanning honks: %s" , err )
return nil
}
h . Date , _ = time . Parse ( dbtimeformat , dt )
h . Audience = strings . Split ( aud , " " )
honks = append ( honks , & h )
}
rows . Close ( )
donksforhonks ( honks )
return honks
}
func donksforhonks ( honks [ ] * Honk ) {
db := opendatabase ( )
var ids [ ] string
2019-04-14 04:57:16 +02:00
hmap := make ( map [ int64 ] * Honk )
2019-04-09 13:59:33 +02:00
for _ , h := range honks {
2019-04-15 22:18:38 +02:00
if h . What == "zonk" {
continue
}
2019-04-09 13:59:33 +02:00
ids = append ( ids , fmt . Sprintf ( "%d" , h . ID ) )
2019-04-14 04:57:16 +02:00
hmap [ h . ID ] = h
2019-04-09 13:59:33 +02:00
}
q := fmt . Sprintf ( "select honkid, donks.fileid, xid, name, url, media from donks join files on donks.fileid = files.fileid where honkid in (%s)" , strings . Join ( ids , "," ) )
rows , err := db . Query ( q )
if err != nil {
log . Printf ( "error querying donks: %s" , err )
return
}
defer rows . Close ( )
for rows . Next ( ) {
var hid int64
var d Donk
err = rows . Scan ( & hid , & d . FileID , & d . XID , & d . Name , & d . URL , & d . Media )
if err != nil {
log . Printf ( "error scanning donk: %s" , err )
continue
}
2019-04-14 04:57:16 +02:00
h := hmap [ hid ]
h . Donks = append ( h . Donks , & d )
2019-04-09 13:59:33 +02:00
}
}
func savebonk ( w http . ResponseWriter , r * http . Request ) {
xid := r . FormValue ( "xid" )
log . Printf ( "bonking %s" , xid )
xonk := getxonk ( "" , xid )
if xonk == nil {
return
}
if xonk . Honker == "" {
xonk . XID = fmt . Sprintf ( "https://%s/u/%s/h/%s" , serverName , xonk . Username , xonk . XID )
}
2019-04-19 17:50:35 +02:00
convoy := xonk . Convoy
2019-04-09 13:59:33 +02:00
userinfo := GetUserInfo ( r )
dt := time . Now ( ) . UTC ( )
bonk := Honk {
UserID : userinfo . UserID ,
Username : userinfo . Username ,
Honker : xonk . Honker ,
What : "bonk" ,
XID : xonk . XID ,
Date : dt ,
Noise : xonk . Noise ,
2019-04-19 17:50:35 +02:00
Convoy : convoy ,
2019-04-09 13:59:33 +02:00
Donks : xonk . Donks ,
Audience : oneofakind ( prepend ( thewholeworld , xonk . Audience ) ) ,
}
aud := strings . Join ( bonk . Audience , " " )
res , err := stmtSaveHonk . Exec ( userinfo . UserID , "bonk" , "" , xid , "" ,
2019-04-19 17:08:22 +02:00
dt . Format ( dbtimeformat ) , "" , aud , bonk . Noise , bonk . Convoy )
2019-04-09 13:59:33 +02:00
if err != nil {
log . Printf ( "error saving bonk: %s" , err )
return
}
bonk . ID , _ = res . LastInsertId ( )
for _ , d := range bonk . Donks {
_ , err = stmtSaveDonk . Exec ( bonk . ID , d . FileID )
if err != nil {
log . Printf ( "err saving donk: %s" , err )
return
}
}
user , _ := butwhatabout ( userinfo . Username )
go honkworldwide ( user , & bonk )
}
2019-04-15 22:18:38 +02:00
func zonkit ( w http . ResponseWriter , r * http . Request ) {
xid := r . FormValue ( "xid" )
log . Printf ( "zonking %s" , xid )
userinfo := GetUserInfo ( r )
stmtZonkIt . Exec ( userinfo . UserID , xid )
}
2019-04-09 13:59:33 +02:00
func savehonk ( w http . ResponseWriter , r * http . Request ) {
rid := r . FormValue ( "rid" )
noise := r . FormValue ( "noise" )
userinfo := GetUserInfo ( r )
dt := time . Now ( ) . UTC ( )
xid := xfiltrate ( )
what := "honk"
if rid != "" {
what = "tonk"
}
honk := Honk {
UserID : userinfo . UserID ,
Username : userinfo . Username ,
What : "honk" ,
XID : xid ,
RID : rid ,
Date : dt ,
}
if noise [ 0 ] == '@' {
honk . Audience = append ( grapevine ( noise ) , thewholeworld )
} else {
2019-04-16 00:03:36 +02:00
honk . Audience = prepend ( thewholeworld , grapevine ( noise ) )
2019-04-09 13:59:33 +02:00
}
2019-04-19 17:50:35 +02:00
var convoy string
2019-04-09 13:59:33 +02:00
if rid != "" {
xonk := getxonk ( "" , rid )
2019-04-11 16:30:53 +02:00
if xonk != nil {
honk . Audience = append ( honk . Audience , xonk . Audience ... )
2019-04-19 17:50:35 +02:00
convoy = xonk . Convoy
2019-04-11 16:44:50 +02:00
} else {
2019-04-19 17:50:35 +02:00
xonkaud , c := whosthere ( rid )
2019-04-11 16:44:50 +02:00
honk . Audience = append ( honk . Audience , xonkaud ... )
2019-04-19 17:50:35 +02:00
convoy = c
2019-04-11 16:30:53 +02:00
}
2019-04-09 13:59:33 +02:00
}
2019-04-19 17:50:35 +02:00
if convoy == "" {
convoy = "data:,electrichonkytonk-" + xfiltrate ( )
}
2019-04-09 13:59:33 +02:00
honk . Audience = oneofakind ( honk . Audience )
noise = obfusbreak ( noise )
honk . Noise = noise
2019-04-19 17:50:35 +02:00
honk . Convoy = convoy
2019-04-09 13:59:33 +02:00
2019-04-15 16:04:41 +02:00
file , filehdr , err := r . FormFile ( "donk" )
2019-04-09 13:59:33 +02:00
if err == nil {
var buf bytes . Buffer
io . Copy ( & buf , file )
file . Close ( )
data := buf . Bytes ( )
2019-04-15 16:04:41 +02:00
xid := xfiltrate ( )
var media , name string
2019-04-09 13:59:33 +02:00
img , format , err := image . Decode ( & buf )
2019-04-15 16:04:41 +02:00
if err == nil {
data , format , err = vacuumwrap ( img , format )
if err != nil {
log . Printf ( "can't vacuum image: %s" , err )
return
}
media = "image/" + format
if format == "jpeg" {
format = "jpg"
}
name = xid + "." + format
xid = name
} else {
maxsize := 100000
if len ( data ) > maxsize {
log . Printf ( "bad image: %s too much text: %d" , err , len ( data ) )
http . Error ( w , "didn't like your attachment" , http . StatusUnsupportedMediaType )
return
}
for i := 0 ; i < len ( data ) ; i ++ {
if data [ i ] < 32 && data [ i ] != '\t' && data [ i ] != '\r' && data [ i ] != '\n' {
log . Printf ( "bad image: %s not text: %d" , err , data [ i ] )
http . Error ( w , "didn't like your attachment" , http . StatusUnsupportedMediaType )
return
}
}
media = "text/plain"
name = filehdr . Filename
if name == "" {
name = xid + ".txt"
}
xid += ".txt"
2019-04-09 13:59:33 +02:00
}
2019-04-15 16:04:41 +02:00
url := fmt . Sprintf ( "https://%s/d/%s" , serverName , xid )
res , err := stmtSaveFile . Exec ( xid , name , url , media , data )
2019-04-09 13:59:33 +02:00
if err != nil {
log . Printf ( "unable to save image: %s" , err )
return
}
var d Donk
d . FileID , _ = res . LastInsertId ( )
d . XID = name
d . Name = name
d . Media = media
d . URL = url
honk . Donks = append ( honk . Donks , & d )
}
2019-04-12 21:26:29 +02:00
herd := herdofemus ( honk . Noise )
for _ , e := range herd {
donk := savedonk ( e . ID , e . Name , "image/png" )
if donk != nil {
2019-04-18 22:34:51 +02:00
donk . Name = e . Name
2019-04-12 21:26:29 +02:00
honk . Donks = append ( honk . Donks , donk )
}
}
2019-04-09 13:59:33 +02:00
aud := strings . Join ( honk . Audience , " " )
res , err := stmtSaveHonk . Exec ( userinfo . UserID , what , "" , xid , rid ,
2019-04-19 17:08:22 +02:00
dt . Format ( dbtimeformat ) , "" , aud , noise , convoy )
2019-04-09 13:59:33 +02:00
if err != nil {
log . Printf ( "error saving honk: %s" , err )
return
}
honk . ID , _ = res . LastInsertId ( )
for _ , d := range honk . Donks {
_ , err = stmtSaveDonk . Exec ( honk . ID , d . FileID )
if err != nil {
log . Printf ( "err saving donk: %s" , err )
return
}
}
user , _ := butwhatabout ( userinfo . Username )
go honkworldwide ( user , & honk )
http . Redirect ( w , r , "/" , http . StatusSeeOther )
}
func showhonkers ( w http . ResponseWriter , r * http . Request ) {
userinfo := GetUserInfo ( r )
templinfo := getInfo ( r )
templinfo [ "Honkers" ] = gethonkers ( userinfo . UserID )
templinfo [ "HonkerCSRF" ] = GetCSRF ( "savehonker" , r )
err := readviews . ExecuteTemplate ( w , "honkers.html" , templinfo )
if err != nil {
log . Print ( err )
}
}
var handfull = make ( map [ string ] string )
var handlock sync . Mutex
func gofish ( name string ) string {
if name [ 0 ] == '@' {
name = name [ 1 : ]
}
m := strings . Split ( name , "@" )
if len ( m ) != 2 {
2019-04-13 19:58:42 +02:00
log . Printf ( "bad fish name: %s" , name )
2019-04-09 13:59:33 +02:00
return ""
}
handlock . Lock ( )
ref , ok := handfull [ name ]
2019-04-16 05:54:29 +02:00
handlock . Unlock ( )
2019-04-09 13:59:33 +02:00
if ok {
return ref
}
j , err := GetJunk ( fmt . Sprintf ( "https://%s/.well-known/webfinger?resource=acct:%s" , m [ 1 ] , name ) )
2019-04-16 05:54:29 +02:00
handlock . Lock ( )
defer handlock . Unlock ( )
2019-04-09 13:59:33 +02:00
if err != nil {
2019-04-13 19:58:42 +02:00
log . Printf ( "failed to go fish %s: %s" , name , err )
2019-04-09 13:59:33 +02:00
handfull [ name ] = ""
return ""
}
links , _ := jsongetarray ( j , "links" )
for _ , l := range links {
href , _ := jsongetstring ( l , "href" )
rel , _ := jsongetstring ( l , "rel" )
t , _ := jsongetstring ( l , "type" )
if rel == "self" && friendorfoe ( t ) {
handfull [ name ] = href
return href
}
}
handfull [ name ] = ""
return ""
}
func savehonker ( w http . ResponseWriter , r * http . Request ) {
name := r . FormValue ( "name" )
url := r . FormValue ( "url" )
peep := r . FormValue ( "peep" )
flavor := "presub"
if peep == "peep" {
flavor = "peep"
}
if url == "" {
return
}
if url [ 0 ] == '@' {
url = gofish ( url )
}
if url == "" {
return
}
u := GetUserInfo ( r )
db := opendatabase ( )
_ , err := db . Exec ( "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)" ,
u . UserID , name , url , flavor )
if err != nil {
log . Print ( err )
}
if flavor == "presub" {
user , _ := butwhatabout ( u . Username )
go subsub ( user , url )
}
http . Redirect ( w , r , "/honkers" , http . StatusSeeOther )
}
2019-04-16 02:52:24 +02:00
func somedays ( ) string {
secs := 432000 + notrand . Int63n ( 432000 )
return fmt . Sprintf ( "%d" , secs )
}
2019-04-09 13:59:33 +02:00
func avatate ( w http . ResponseWriter , r * http . Request ) {
n := r . FormValue ( "a" )
a := avatar ( n )
2019-04-16 02:52:24 +02:00
w . Header ( ) . Set ( "Cache-Control" , "max-age=" + somedays ( ) )
2019-04-09 13:59:33 +02:00
w . Write ( a )
}
func servecss ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Set ( "Cache-Control" , "max-age=7776000" )
http . ServeFile ( w , r , "views" + r . URL . Path )
}
func servehtml ( w http . ResponseWriter , r * http . Request ) {
templinfo := getInfo ( r )
err := readviews . ExecuteTemplate ( w , r . URL . Path [ 1 : ] + ".html" , templinfo )
if err != nil {
log . Print ( err )
}
}
2019-04-12 21:02:56 +02:00
func serveemu ( w http . ResponseWriter , r * http . Request ) {
xid := mux . Vars ( r ) [ "xid" ]
2019-04-16 02:52:24 +02:00
w . Header ( ) . Set ( "Cache-Control" , "max-age=" + somedays ( ) )
2019-04-12 21:02:56 +02:00
http . ServeFile ( w , r , "emus/" + xid )
}
2019-04-09 13:59:33 +02:00
func servefile ( w http . ResponseWriter , r * http . Request ) {
xid := mux . Vars ( r ) [ "xid" ]
row := stmtFileData . QueryRow ( xid )
2019-04-15 16:04:41 +02:00
var media string
2019-04-09 13:59:33 +02:00
var data [ ] byte
2019-04-15 16:04:41 +02:00
err := row . Scan ( & media , & data )
2019-04-09 13:59:33 +02:00
if err != nil {
log . Printf ( "error loading file: %s" , err )
http . NotFound ( w , r )
return
}
2019-04-15 16:04:41 +02:00
w . Header ( ) . Set ( "Content-Type" , media )
2019-04-17 18:32:50 +02:00
w . Header ( ) . Set ( "X-Content-Type-Options" , "nosniff" )
2019-04-16 02:52:24 +02:00
w . Header ( ) . Set ( "Cache-Control" , "max-age=" + somedays ( ) )
2019-04-09 13:59:33 +02:00
w . Write ( data )
}
func serve ( ) {
db := opendatabase ( )
LoginInit ( db )
listener , err := openListener ( )
if err != nil {
log . Fatal ( err )
}
2019-04-15 03:35:42 +02:00
go redeliverator ( )
2019-04-09 13:59:33 +02:00
debug := false
getconfig ( "debug" , & debug )
readviews = ParseTemplates ( debug ,
"views/homepage.html" ,
"views/honkpage.html" ,
"views/honkers.html" ,
"views/honkform.html" ,
"views/honk.html" ,
"views/login.html" ,
"views/header.html" ,
)
if ! debug {
2019-04-12 19:12:18 +02:00
s := "views/style.css"
savedstyleparams [ s ] = getstyleparam ( s )
s = "views/local.css"
savedstyleparams [ s ] = getstyleparam ( s )
2019-04-09 13:59:33 +02:00
}
mux := mux . NewRouter ( )
mux . Use ( LoginChecker )
posters := mux . Methods ( "POST" ) . Subrouter ( )
getters := mux . Methods ( "GET" ) . Subrouter ( )
getters . HandleFunc ( "/" , homepage )
getters . HandleFunc ( "/rss" , showrss )
getters . HandleFunc ( "/u/{name:[[:alnum:]]+}" , viewuser )
getters . HandleFunc ( "/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}" , viewhonk )
getters . HandleFunc ( "/u/{name:[[:alnum:]]+}/rss" , showrss )
posters . HandleFunc ( "/u/{name:[[:alnum:]]+}/inbox" , inbox )
getters . HandleFunc ( "/u/{name:[[:alnum:]]+}/outbox" , outbox )
getters . HandleFunc ( "/a" , avatate )
getters . HandleFunc ( "/d/{xid:[[:alnum:].]+}" , servefile )
2019-04-12 21:18:18 +02:00
getters . HandleFunc ( "/emu/{xid:[[:alnum:]_.]+}" , serveemu )
2019-04-09 13:59:33 +02:00
getters . HandleFunc ( "/h/{name:[[:alnum:]]+}" , viewhonker )
getters . HandleFunc ( "/.well-known/webfinger" , fingerlicker )
getters . HandleFunc ( "/style.css" , servecss )
2019-04-12 19:12:18 +02:00
getters . HandleFunc ( "/local.css" , servecss )
2019-04-09 13:59:33 +02:00
getters . HandleFunc ( "/login" , servehtml )
posters . HandleFunc ( "/dologin" , dologin )
getters . HandleFunc ( "/logout" , dologout )
loggedin := mux . NewRoute ( ) . Subrouter ( )
loggedin . Use ( LoginRequired )
loggedin . Handle ( "/honk" , CSRFWrap ( "honkhonk" , http . HandlerFunc ( savehonk ) ) )
loggedin . Handle ( "/bonk" , CSRFWrap ( "honkhonk" , http . HandlerFunc ( savebonk ) ) )
2019-04-15 22:18:38 +02:00
loggedin . Handle ( "/zonkit" , CSRFWrap ( "honkhonk" , http . HandlerFunc ( zonkit ) ) )
2019-04-09 13:59:33 +02:00
loggedin . Handle ( "/saveuser" , CSRFWrap ( "saveuser" , http . HandlerFunc ( saveuser ) ) )
loggedin . HandleFunc ( "/honkers" , showhonkers )
loggedin . Handle ( "/savehonker" , CSRFWrap ( "savehonker" , http . HandlerFunc ( savehonker ) ) )
err = http . Serve ( listener , mux )
if err != nil {
log . Fatal ( err )
}
}
2019-04-14 04:57:16 +02:00
var stmtHonkers , stmtDubbers , stmtOneXonk , stmtHonks , stmtUserHonks * sql . Stmt
2019-04-09 13:59:33 +02:00
var stmtHonksForUser , stmtDeleteHonk , stmtSaveDub * sql . Stmt
var stmtHonksByHonker , stmtSaveHonk , stmtFileData , stmtWhatAbout * sql . Stmt
var stmtFindXonk , stmtSaveDonk , stmtFindFile , stmtSaveFile * sql . Stmt
2019-04-15 03:35:42 +02:00
var stmtAddDoover , stmtGetDoovers , stmtLoadDoover , stmtZapDoover * sql . Stmt
2019-04-15 22:18:38 +02:00
var stmtZonkIt * sql . Stmt
2019-04-09 13:59:33 +02:00
2019-04-14 23:09:08 +02:00
func preparetodie ( db * sql . DB , s string ) * sql . Stmt {
stmt , err := db . Prepare ( s )
2019-04-09 13:59:33 +02:00
if err != nil {
2019-04-16 17:16:43 +02:00
log . Fatalf ( "error %s: %s" , err , s )
2019-04-09 13:59:33 +02:00
}
2019-04-14 23:09:08 +02:00
return stmt
}
func prepareStatements ( db * sql . DB ) {
stmtHonkers = preparetodie ( db , "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'sub' or flavor = 'peep'" )
stmtDubbers = preparetodie ( db , "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'" )
2019-04-19 17:08:22 +02:00
stmtOneXonk = preparetodie ( db , "select honkid, honks.userid, users.username, what, honker, xid, rid, dt, url, audience, noise, convoy from honks join users on honks.userid = users.userid where xid = ?" )
stmtHonks = preparetodie ( db , "select honkid, honks.userid, users.username, what, honker, xid, rid, dt, url, audience, noise, convoy from honks join users on honks.userid = users.userid where honker = '' order by honkid desc limit 50" )
stmtUserHonks = preparetodie ( db , "select honkid, honks.userid, username, what, honker, xid, rid, dt, url, audience, noise, convoy from honks join users on honks.userid = users.userid where honker = '' and username = ? order by honkid desc limit 50" )
stmtHonksForUser = preparetodie ( db , "select honkid, honks.userid, users.username, what, honker, xid, rid, dt, url, audience, noise, convoy from honks join users on honks.userid = users.userid where honks.userid = ? and dt > ? order by honkid desc limit 250" )
stmtHonksByHonker = preparetodie ( db , "select honkid, honks.userid, users.username, what, honker, honks.xid, rid, dt, url, audience, noise, convoy from honks join users on honks.userid = users.userid join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ? order by honkid desc limit 50" )
stmtSaveHonk = preparetodie ( db , "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" )
2019-04-15 16:04:41 +02:00
stmtFileData = preparetodie ( db , "select media, content from files where xid = ?" )
2019-04-14 23:09:08 +02:00
stmtFindXonk = preparetodie ( db , "select honkid from honks where userid = ? and xid = ?" )
stmtSaveDonk = preparetodie ( db , "insert into donks (honkid, fileid) values (?, ?)" )
stmtDeleteHonk = preparetodie ( db , "update honks set what = 'zonk' where xid = ? and honker = ?" )
stmtFindFile = preparetodie ( db , "select fileid from files where url = ?" )
stmtSaveFile = preparetodie ( db , "insert into files (xid, name, url, media, content) values (?, ?, ?, ?, ?)" )
stmtWhatAbout = preparetodie ( db , "select userid, username, displayname, about, pubkey from users where username = ?" )
stmtSaveDub = preparetodie ( db , "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)" )
2019-04-15 03:35:42 +02:00
stmtAddDoover = preparetodie ( db , "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)" )
stmtGetDoovers = preparetodie ( db , "select dooverid, dt from doovers" )
stmtLoadDoover = preparetodie ( db , "select tries, username, rcpt, msg from doovers where dooverid = ?" )
stmtZapDoover = preparetodie ( db , "delete from doovers where dooverid = ?" )
2019-04-15 22:18:38 +02:00
stmtZonkIt = preparetodie ( db , "update honks set what = 'zonk' where userid = ? and xid = ?" )
2019-04-09 13:59:33 +02:00
}
func ElaborateUnitTests ( ) {
}
func finishusersetup ( ) error {
db := opendatabase ( )
k , err := rsa . GenerateKey ( rand . Reader , 2048 )
if err != nil {
return err
}
pubkey , err := zem ( & k . PublicKey )
if err != nil {
return err
}
seckey , err := zem ( k )
if err != nil {
return err
}
_ , err = db . Exec ( "update users set displayname = username, about = ?, pubkey = ?, seckey = ? where userid = 1" , "what about me?" , pubkey , seckey )
if err != nil {
return err
}
return nil
}
func main ( ) {
cmd := "run"
if len ( os . Args ) > 1 {
cmd = os . Args [ 1 ]
}
2019-04-14 22:43:03 +02:00
switch cmd {
case "init" :
initdb ( )
case "upgrade" :
upgradedb ( )
2019-04-09 13:59:33 +02:00
}
2019-04-14 22:43:03 +02:00
db := opendatabase ( )
dbversion := 0
getconfig ( "dbversion" , & dbversion )
if dbversion != myVersion {
log . Fatal ( "incorrect database version. run upgrade." )
}
getconfig ( "servername" , & serverName )
prepareStatements ( db )
2019-04-09 13:59:33 +02:00
switch cmd {
2019-04-12 18:32:07 +02:00
case "ping" :
if len ( os . Args ) < 4 {
fmt . Printf ( "usage: honk ping from to\n" )
return
}
name := os . Args [ 2 ]
targ := os . Args [ 3 ]
user , err := butwhatabout ( name )
if err != nil {
log . Printf ( "unknown user" )
return
}
ping ( user , targ )
2019-04-09 13:59:33 +02:00
case "peep" :
peeppeep ( )
case "run" :
serve ( )
case "test" :
ElaborateUnitTests ( )
default :
log . Fatal ( "unknown command" )
}
}